Bug 31963: Only show hold fee msg on OPAC if patron will be charged
[koha.git] / C4 / Reserves.pm
1 package C4::Reserves;
2
3 # Copyright 2000-2002 Katipo Communications
4 #           2006 SAN Ouest Provence
5 #           2007-2010 BibLibre Paul POULAIN
6 #           2011 Catalyst IT
7 #
8 # This file is part of Koha.
9 #
10 # Koha is free software; you can redistribute it and/or modify it
11 # under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
14 #
15 # Koha is distributed in the hope that it will be useful, but
16 # WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
19 #
20 # You should have received a copy of the GNU General Public License
21 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22
23
24 use Modern::Perl;
25
26 use C4::Accounts;
27 use C4::Biblio qw( GetMarcFromKohaField );
28 use C4::Circulation qw( CheckIfIssuedToPatron GetAgeRestriction GetBranchItemRule );
29 use C4::Context;
30 use C4::Items qw( CartToShelf get_hostitemnumbers_of );
31 use C4::Letters;
32 use C4::Log qw( logaction );
33 use C4::Members::Messaging;
34 use C4::Members;
35 use Koha::Account::Lines;
36 use Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue;
37 use Koha::Biblios;
38 use Koha::Calendar;
39 use Koha::CirculationRules;
40 use Koha::Database;
41 use Koha::DateUtils qw( dt_from_string output_pref );
42 use Koha::Holds;
43 use Koha::ItemTypes;
44 use Koha::Items;
45 use Koha::Libraries;
46 use Koha::Old::Holds;
47 use Koha::Patrons;
48 use Koha::Plugins;
49
50 use List::MoreUtils qw( any );
51
52 =head1 NAME
53
54 C4::Reserves - Koha functions for dealing with reservation.
55
56 =head1 SYNOPSIS
57
58   use C4::Reserves;
59
60 =head1 DESCRIPTION
61
62 This modules provides somes functions to deal with reservations.
63
64   Reserves are stored in reserves table.
65   The following columns contains important values :
66   - priority >0      : then the reserve is at 1st stage, and not yet affected to any item.
67              =0      : then the reserve is being dealed
68   - found : NULL         : means the patron requested the 1st available, and we haven't chosen the item
69             T(ransit)    : the reserve is linked to an item but is in transit to the pickup branch
70             W(aiting)    : the reserve is linked to an item, is at the pickup branch, and is waiting on the hold shelf
71             F(inished)   : the reserve has been completed, and is done
72             P(rocessing) : reserved item has been returned using self-check machine and reserve needs to be confirmed
73                            by librarian before notice is send and status changed to waiting.
74                            Applicable only if HoldsNeedProcessingSIP system preference is set.
75   - itemnumber : empty : the reserve is still unaffected to an item
76                  filled: the reserve is attached to an item
77   The complete workflow is :
78   ==== 1st use case ====
79   patron request a document, 1st available :                      P >0, F=NULL, I=NULL
80   a library having it run "transfertodo", and clic on the list
81          if there is no transfer to do, the reserve waiting
82          patron can pick it up                                    P =0, F=W,    I=filled
83          if there is a transfer to do, write in branchtransfer    P =0, F=T,    I=filled
84            The pickup library receive the book, it check in       P =0, F=W,    I=filled
85   The patron borrow the book                                      P =0, F=F,    I=filled
86
87   ==== 2nd use case ====
88   patron requests a document, a given item,
89     If pickup is holding branch                                   P =0, F=W,   I=filled
90     If transfer needed, write in branchtransfer                   P =0, F=T,    I=filled
91         The pickup library receive the book, it checks it in      P =0, F=W,    I=filled
92   The patron borrow the book                                      P =0, F=F,    I=filled
93
94 =head1 FUNCTIONS
95
96 =cut
97
98 our (@ISA, @EXPORT_OK);
99 BEGIN {
100     require Exporter;
101     @ISA = qw(Exporter);
102     @EXPORT_OK = qw(
103       AddReserve
104
105       GetReserveStatus
106
107       GetOtherReserves
108       ChargeReserveFee
109       GetReserveFee
110
111       ModReserveAffect
112       ModReserve
113       ModReserveStatus
114       ModReserveCancelAll
115       ModReserveMinusPriority
116       MoveReserve
117
118       CheckReserves
119       CanBookBeReserved
120       CanItemBeReserved
121       CanReserveBeCanceledFromOpac
122       CancelExpiredReserves
123
124       AutoUnsuspendReserves
125
126       IsAvailableForItemLevelRequest
127       ItemsAnyAvailableAndNotRestricted
128
129       AlterPriority
130       ToggleLowestPriority
131
132       ReserveSlip
133       ToggleSuspend
134       SuspendAll
135
136       GetReservesControlBranch
137
138       CalculatePriority
139
140       IsItemOnHoldAndFound
141
142       GetMaxPatronHoldsForRecord
143
144       MergeHolds
145
146       RevertWaitingStatus
147     );
148 }
149
150 =head2 AddReserve
151
152     AddReserve(
153         {
154             branchcode       => $branchcode,
155             borrowernumber   => $borrowernumber,
156             biblionumber     => $biblionumber,
157             priority         => $priority,
158             reservation_date => $reservation_date,
159             expiration_date  => $expiration_date,
160             notes            => $notes,
161             title            => $title,
162             itemnumber       => $itemnumber,
163             found            => $found,
164             itemtype         => $itemtype,
165         }
166     );
167
168 Adds reserve and generates HOLDPLACED message.
169
170 The following tables are available witin the HOLDPLACED message:
171
172     branches
173     borrowers
174     biblio
175     biblioitems
176     items
177     reserves
178
179 =cut
180
181 sub AddReserve {
182     my ($params)       = @_;
183     my $branch         = $params->{branchcode};
184     my $borrowernumber = $params->{borrowernumber};
185     my $biblionumber   = $params->{biblionumber};
186     my $priority       = $params->{priority};
187     my $resdate        = $params->{reservation_date};
188     my $patron_expiration_date = $params->{expiration_date};
189     my $notes          = $params->{notes};
190     my $title          = $params->{title};
191     my $checkitem      = $params->{itemnumber};
192     my $found          = $params->{found};
193     my $itemtype       = $params->{itemtype};
194     my $non_priority   = $params->{non_priority};
195
196     $resdate ||= dt_from_string;
197
198     # if we have an item selectionned, and the pickup branch is the same as the holdingbranch
199     # of the document, we force the value $priority and $found .
200     if ( $checkitem and not C4::Context->preference('ReservesNeedReturns') ) {
201         my $item = Koha::Items->find( $checkitem ); # FIXME Prevent bad calls
202
203         if (
204             # If item is already checked out, it cannot be set waiting
205             !$item->onloan
206
207             # The item can't be waiting if it needs a transfer
208             && $item->holdingbranch eq $branch
209
210             # Similarly, if in transit it can't be waiting
211             && !$item->get_transfer
212
213             # If we can't hold damaged items, and it is damaged, it can't be waiting
214             && ( $item->damaged && C4::Context->preference('AllowHoldsOnDamagedItems') || !$item->damaged )
215
216             # Lastly, if this already has holds, we shouldn't make it waiting for the new hold
217             && !$item->current_holds->count )
218         {
219             $priority = 0;
220             $found = 'W';
221         }
222     }
223     if ( C4::Context->preference( 'AllowHoldDateInFuture' ) ) {
224     # Make room in reserves for this if passed a priority
225     $priority = _ShiftPriority( $biblionumber, $priority );
226     }
227
228     my $waitingdate;
229
230     # If the reserv had the waiting status, we had the value of the resdate
231     if ( $found && $found eq 'W' ) {
232         $waitingdate = $resdate;
233     }
234
235     # Don't add itemtype limit if specific item is selected
236     $itemtype = undef if $checkitem;
237
238     # updates take place here
239     my $hold = Koha::Hold->new(
240         {
241             borrowernumber => $borrowernumber,
242             biblionumber   => $biblionumber,
243             reservedate    => $resdate,
244             branchcode     => $branch,
245             priority       => $priority,
246             reservenotes   => $notes,
247             itemnumber     => $checkitem,
248             found          => $found,
249             waitingdate    => $waitingdate,
250             patron_expiration_date => $patron_expiration_date,
251             itemtype       => $itemtype,
252             item_level_hold => $checkitem ? 1 : 0,
253             non_priority   => $non_priority ? 1 : 0,
254         }
255     )->store();
256     $hold->set_waiting() if $found && $found eq 'W';
257
258     logaction( 'HOLDS', 'CREATE', $hold->id, $hold )
259         if C4::Context->preference('HoldsLog');
260
261     my $reserve_id = $hold->id();
262
263     # add a reserve fee if needed
264     if ( C4::Context->preference('HoldFeeMode') ne 'any_time_is_collected' ) {
265         my $reserve_fee = GetReserveFee( $borrowernumber, $biblionumber );
266         ChargeReserveFee( $borrowernumber, $reserve_fee, $title );
267     }
268
269     _FixPriority({ biblionumber => $biblionumber});
270
271     # Send e-mail to librarian if syspref is active
272     if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
273         my $patron = Koha::Patrons->find( $borrowernumber );
274         my $library = $patron->library;
275         if ( my $letter =  C4::Letters::GetPreparedLetter (
276             module => 'reserves',
277             letter_code => 'HOLDPLACED',
278             branchcode => $branch,
279             lang => $patron->lang,
280             tables => {
281                 'branches'    => $library->unblessed,
282                 'borrowers'   => $patron->unblessed,
283                 'biblio'      => $biblionumber,
284                 'biblioitems' => $biblionumber,
285                 'items'       => $checkitem,
286                 'reserves'    => $hold->unblessed,
287             },
288         ) ) {
289
290             my $branch_email_address = $library->inbound_email_address;
291
292             C4::Letters::EnqueueLetter(
293                 {
294                     letter                 => $letter,
295                     borrowernumber         => $borrowernumber,
296                     message_transport_type => 'email',
297                     to_address             => $branch_email_address,
298                 }
299             );
300         }
301     }
302
303     Koha::Plugins->call('after_hold_create', $hold);
304     Koha::Plugins->call(
305         'after_hold_action',
306         {
307             action  => 'place',
308             payload => { hold => $hold->get_from_storage }
309         }
310     );
311
312     Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
313         {
314             biblio_ids => [ $biblionumber ]
315         }
316     ) if C4::Context->preference('RealTimeHoldsQueue');
317
318     return $reserve_id;
319 }
320
321 =head2 CanBookBeReserved
322
323   $canReserve = &CanBookBeReserved($borrowernumber, $biblionumber, $branchcode, $params)
324   if ($canReserve eq 'OK') { #We can reserve this Item! }
325
326   $params are passed directly through to CanItemBeReserved
327
328 See CanItemBeReserved() for possible return values.
329
330 =cut
331
332 sub CanBookBeReserved{
333     my ($borrowernumber, $biblionumber, $pickup_branchcode, $params) = @_;
334
335     # Check that patron have not checked out this biblio (if AllowHoldsOnPatronsPossessions set)
336     if ( !C4::Context->preference('AllowHoldsOnPatronsPossessions')
337         && C4::Circulation::CheckIfIssuedToPatron( $borrowernumber, $biblionumber ) ) {
338         return { status =>'alreadypossession' };
339     }
340
341     if ( $params->{itemtype} ) {
342
343         # biblio-level, item type-contrained
344         my $patron          = Koha::Patrons->find($borrowernumber);
345         my $reservesallowed = Koha::CirculationRules->get_effective_rule(
346             {
347                 itemtype     => $params->{itemtype},
348                 categorycode => $patron->categorycode,
349                 branchcode   => $pickup_branchcode,
350                 rule_name    => 'reservesallowed',
351             }
352         )->rule_value;
353
354         $reservesallowed = ( $reservesallowed eq '' ) ? undef : $reservesallowed;
355
356         my $count = $patron->holds->search(
357             {
358                 '-or' => [
359                     { 'me.itemtype' => $params->{itemtype} },
360                     { 'item.itype'  => $params->{itemtype} }
361                 ]
362             },
363             {
364                 join => ['item']
365             }
366         )->count;
367
368         return { status => '' }
369           if defined $reservesallowed and $reservesallowed < $count + 1;
370     }
371
372     my $items;
373     #get items linked via host records
374     my @hostitemnumbers = get_hostitemnumbers_of($biblionumber);
375     if (@hostitemnumbers){
376         $items = Koha::Items->search({
377             -or => [
378                 biblionumber => $biblionumber,
379                 itemnumber => { -in => @hostitemnumbers }
380             ]
381         });
382     } else {
383         $items = Koha::Items->search({ biblionumber => $biblionumber});
384     }
385
386     my $canReserve = { status => '' };
387     my $patron = Koha::Patrons->find( $borrowernumber );
388     while ( my $item = $items->next ) {
389         $canReserve = CanItemBeReserved( $patron, $item, $pickup_branchcode, $params );
390         return { status => 'OK' } if $canReserve->{status} eq 'OK';
391     }
392     return $canReserve;
393 }
394
395 =head2 CanItemBeReserved
396
397   $canReserve = &CanItemBeReserved($patron, $item, $branchcode, $params)
398   if ($canReserve->{status} eq 'OK') { #We can reserve this Item! }
399
400   current params are:
401   'ignore_found_holds' - if true holds that have been trapped are not counted
402   toward the patron limit, used by checkHighHolds to avoid counting the hold we will fill with the
403   current checkout against the high holds threshold
404   'ignore_hold_counts' - we use this routine to check if an item can fill a hold - on this case we
405   should not check if there are too many holds as we only csre about reservability
406
407 @RETURNS { status => OK },              if the Item can be reserved.
408          { status => ageRestricted },   if the Item is age restricted for this borrower.
409          { status => damaged },         if the Item is damaged.
410          { status => cannotReserveFromOtherBranches }, if syspref 'canreservefromotherbranches' is OK.
411          { status => branchNotInHoldGroup }, if borrower home library is not in hold group, and holds are only allowed from hold groups.
412          { status => tooManyReserves, limit => $limit }, if the borrower has exceeded their maximum reserve amount.
413          { status => notReservable },   if holds on this item are not allowed
414          { status => libraryNotFound },   if given branchcode is not an existing library
415          { status => libraryNotPickupLocation },   if given branchcode is not configured to be a pickup location
416          { status => cannotBeTransferred }, if branch transfer limit applies on given item and branchcode
417          { status => pickupNotInHoldGroup }, pickup location is not in hold group, and pickup locations are only allowed from hold groups.
418          { status => recall }, if the borrower has already placed a recall on this item
419
420 =cut
421
422 sub CanItemBeReserved {
423     my ( $patron, $item, $pickup_branchcode, $params ) = @_;
424
425     my $dbh = C4::Context->dbh;
426     my $ruleitemtype;    # itemtype of the matching issuing rule
427     my $allowedreserves  = 0; # Total number of holds allowed across all records, default to none
428
429     # We check item branch if IndependentBranches is ON
430     # and canreservefromotherbranches is OFF
431     if ( C4::Context->preference('IndependentBranches')
432         and !C4::Context->preference('canreservefromotherbranches') )
433     {
434         if ( $item->homebranch ne $patron->branchcode ) {
435             return { status => 'cannotReserveFromOtherBranches' };
436         }
437     }
438
439     # we retrieve borrowers and items informations #
440     # item->{itype} will come for biblioitems if necessery
441     my $borrower = $patron->unblessed;
442
443     # If an item is damaged and we don't allow holds on damaged items, we can stop right here
444     return { status =>'damaged' }
445       if ( $item->damaged
446         && !C4::Context->preference('AllowHoldsOnDamagedItems') );
447
448     if( GetMarcFromKohaField('biblioitems.agerestriction') ){
449         my $biblio = $item->biblio;
450         # Check for the age restriction
451         my ( $ageRestriction, $daysToAgeRestriction ) =
452           C4::Circulation::GetAgeRestriction( $biblio->biblioitem->agerestriction, $borrower );
453         return { status => 'ageRestricted' } if $daysToAgeRestriction && $daysToAgeRestriction > 0;
454     }
455
456     # Check that the patron doesn't have an item level hold on this item already
457     return { status =>'itemAlreadyOnHold' }
458       if ( !$params->{ignore_hold_counts} && Koha::Holds->search( { borrowernumber => $patron->borrowernumber, itemnumber => $item->itemnumber } )->count() );
459
460     # Check that patron have not checked out this biblio (if AllowHoldsOnPatronsPossessions set)
461     if ( !C4::Context->preference('AllowHoldsOnPatronsPossessions')
462         && C4::Circulation::CheckIfIssuedToPatron( $patron->borrowernumber, $item->biblionumber ) ) {
463         return { status =>'alreadypossession' };
464     }
465
466     # check if a recall exists on this item from this borrower
467     return { status => 'recall' }
468       if $patron->recalls->filter_by_current->search({ item_id => $item->itemnumber })->count;
469
470     my $controlbranch = C4::Context->preference('ReservesControlBranch');
471
472     my $reserves_control_branch;
473     my $branchfield = "reserves.branchcode";
474
475     if ( $controlbranch eq "ItemHomeLibrary" ) {
476         $branchfield = "items.homebranch";
477         $reserves_control_branch  = $item->homebranch;
478     }
479     elsif ( $controlbranch eq "PatronLibrary" ) {
480         $branchfield = "borrowers.branchcode";
481         $reserves_control_branch  = $borrower->{branchcode};
482     }
483
484     # we retrieve rights
485     if (
486         my $reservesallowed = Koha::CirculationRules->get_effective_rule({
487                 itemtype     => $item->effective_itemtype,
488                 categorycode => $borrower->{categorycode},
489                 branchcode   => $reserves_control_branch,
490                 rule_name    => 'reservesallowed',
491         })
492     ) {
493         $ruleitemtype     = $reservesallowed->itemtype;
494         $allowedreserves  = $reservesallowed->rule_value // 0; #undefined is 0, blank is unlimited
495     }
496     else {
497         $ruleitemtype = undef;
498     }
499
500     my $rights = Koha::CirculationRules->get_effective_rules({
501         categorycode => $borrower->{'categorycode'},
502         itemtype     => $item->effective_itemtype,
503         branchcode   => $reserves_control_branch,
504         rules        => ['holds_per_record','holds_per_day']
505     });
506     my $holds_per_record = $rights->{holds_per_record} // 1;
507     my $holds_per_day    = $rights->{holds_per_day};
508
509     if (   defined $holds_per_record && $holds_per_record ne '' ){
510         if ( $holds_per_record == 0 ) {
511             return { status => "noReservesAllowed" };
512         }
513         if ( !$params->{ignore_hold_counts} ) {
514             my $search_params = {
515                 borrowernumber => $patron->borrowernumber,
516                 biblionumber   => $item->biblionumber,
517             };
518             $search_params->{found} = undef if $params->{ignore_found_holds};
519             my $holds = Koha::Holds->search($search_params);
520             return { status => "tooManyHoldsForThisRecord", limit => $holds_per_record } if $holds->count() >= $holds_per_record;
521         }
522     }
523
524     if (!$params->{ignore_hold_counts} && defined $holds_per_day && $holds_per_day ne '')
525     {
526         my $today_holds = Koha::Holds->search({
527             borrowernumber => $patron->borrowernumber,
528             reservedate    => dt_from_string->date
529         });
530         return { status => 'tooManyReservesToday', limit => $holds_per_day } if $today_holds->count() >= $holds_per_day;
531     }
532
533     # we check if it's ok or not
534     if ( defined $allowedreserves && $allowedreserves ne '' ){
535         if( $allowedreserves == 0 ){
536             return { status => 'noReservesAllowed' };
537         }
538         if ( !$params->{ignore_hold_counts} ) {
539             # we retrieve count
540             my $querycount = q{
541                 SELECT count(*) AS count
542                   FROM reserves
543              LEFT JOIN items USING (itemnumber)
544              LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
545              LEFT JOIN borrowers USING (borrowernumber)
546                  WHERE borrowernumber = ?
547             };
548             $querycount .= "AND ( $branchfield = ? OR $branchfield IS NULL )";
549
550             # If using item-level itypes, fall back to the record
551             # level itemtype if the hold has no associated item
552             if ( defined $ruleitemtype ) {
553                 if ( C4::Context->preference('item-level_itypes') ) {
554                     $querycount .= q{
555                         AND ( COALESCE( items.itype, biblioitems.itemtype ) = ?
556                            OR reserves.itemtype = ? )
557                     };
558                 }
559                 else {
560                     $querycount .= q{
561                         AND ( biblioitems.itemtype = ?
562                            OR reserves.itemtype = ? )
563                     };
564                 }
565             }
566
567             my $sthcount = $dbh->prepare($querycount);
568
569             if ( defined $ruleitemtype ) {
570                 $sthcount->execute( $patron->borrowernumber, $reserves_control_branch, $ruleitemtype, $ruleitemtype );
571             }
572             else {
573                 $sthcount->execute( $patron->borrowernumber, $reserves_control_branch );
574             }
575
576             my $reservecount = "0";
577             if ( my $rowcount = $sthcount->fetchrow_hashref() ) {
578                 $reservecount = $rowcount->{count};
579             }
580
581             return { status => 'tooManyReserves', limit => $allowedreserves } if $reservecount >= $allowedreserves;
582         }
583     }
584
585     # Now we need to check hold limits by patron category
586     my $rule = Koha::CirculationRules->get_effective_rule(
587         {
588             categorycode => $patron->categorycode,
589             branchcode   => $reserves_control_branch,
590             rule_name    => 'max_holds',
591         }
592     );
593     if (!$params->{ignore_hold_counts} && $rule && defined( $rule->rule_value ) && $rule->rule_value ne '' ) {
594         my $total_holds_count = Koha::Holds->search(
595             {
596                 borrowernumber => $patron->borrowernumber
597             }
598         )->count();
599
600         return { status => 'tooManyReserves', limit => $rule->rule_value} if $total_holds_count >= $rule->rule_value;
601     }
602
603     my $branchitemrule =
604       C4::Circulation::GetBranchItemRule( $reserves_control_branch, $item->effective_itemtype );
605
606     if ( $branchitemrule->{holdallowed} eq 'not_allowed' ) {
607         return { status => 'notReservable' };
608     }
609
610     if (   $branchitemrule->{holdallowed} eq 'from_home_library'
611         && $borrower->{branchcode} ne $item->homebranch )
612     {
613         return { status => 'cannotReserveFromOtherBranches' };
614     }
615
616     my $item_library = Koha::Libraries->find( {branchcode => $item->homebranch} );
617     if ( $branchitemrule->{holdallowed} eq 'from_local_hold_group') {
618         if($patron->branchcode ne $item->homebranch && !$item_library->validate_hold_sibling( {branchcode => $patron->branchcode} )) {
619             return { status => 'branchNotInHoldGroup' };
620         }
621     }
622
623     if ($pickup_branchcode) {
624         my $destination = Koha::Libraries->find({
625             branchcode => $pickup_branchcode,
626         });
627
628         unless ($destination) {
629             return { status => 'libraryNotFound' };
630         }
631         unless ($destination->pickup_location) {
632             return { status => 'libraryNotPickupLocation' };
633         }
634         unless ($item->can_be_transferred({ to => $destination })) {
635             return { status => 'cannotBeTransferred' };
636         }
637         if ($branchitemrule->{hold_fulfillment_policy} eq 'holdgroup' && !$item_library->validate_hold_sibling( {branchcode => $pickup_branchcode} )) {
638             return { status => 'pickupNotInHoldGroup' };
639         }
640         if ($branchitemrule->{hold_fulfillment_policy} eq 'patrongroup' && !Koha::Libraries->find({branchcode => $borrower->{branchcode}})->validate_hold_sibling({branchcode => $pickup_branchcode})) {
641             return { status => 'pickupNotInHoldGroup' };
642         }
643     }
644
645     return { status => 'OK' };
646 }
647
648 =head2 CanReserveBeCanceledFromOpac
649
650     $number = CanReserveBeCanceledFromOpac($reserve_id, $borrowernumber);
651
652     returns 1 if reserve can be cancelled by user from OPAC.
653     First check if reserve belongs to user, next checks if reserve is not in
654     transfer or waiting status
655
656 =cut
657
658 sub CanReserveBeCanceledFromOpac {
659     my ($reserve_id, $borrowernumber) = @_;
660
661     return unless $reserve_id and $borrowernumber;
662     my $reserve = Koha::Holds->find($reserve_id) or return;
663
664     return 0 unless $reserve->borrowernumber == $borrowernumber;
665     return $reserve->is_cancelable_from_opac;
666 }
667
668 =head2 GetOtherReserves
669
670   ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
671
672 Check queued list of this document and check if this document must be transferred
673
674 =cut
675
676 sub GetOtherReserves {
677     my ($itemnumber) = @_;
678     my $messages;
679     my $nextreservinfo;
680     my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber);
681     if ($checkreserves) {
682         my $item = Koha::Items->find($itemnumber);
683         if ( $item->holdingbranch ne $checkreserves->{'branchcode'} ) {
684             $messages->{'transfert'} = $checkreserves->{'branchcode'};
685             #minus priorities of others reservs
686             ModReserveMinusPriority(
687                 $itemnumber,
688                 $checkreserves->{'reserve_id'},
689             );
690
691             #launch the subroutine dotransfer
692             C4::Items::ModItemTransfer(
693                 $itemnumber,
694                 $item->holdingbranch,
695                 $checkreserves->{'branchcode'},
696                 'Reserve'
697               ),
698               ;
699         }
700
701      #step 2b : case of a reservation on the same branch, set the waiting status
702         else {
703             $messages->{'waiting'} = 1;
704             ModReserveMinusPriority(
705                 $itemnumber,
706                 $checkreserves->{'reserve_id'},
707             );
708             ModReserveStatus($itemnumber,'W');
709         }
710
711         $nextreservinfo = $checkreserves;
712     }
713
714     return ( $messages, $nextreservinfo );
715 }
716
717 =head2 ChargeReserveFee
718
719     $fee = ChargeReserveFee( $borrowernumber, $fee, $title );
720
721     Charge the fee for a reserve (if $fee > 0)
722
723 =cut
724
725 sub ChargeReserveFee {
726     my ( $borrowernumber, $fee, $title ) = @_;
727     return if !$fee || $fee == 0;    # the last test is needed to include 0.00
728     Koha::Account->new( { patron_id => $borrowernumber } )->add_debit(
729         {
730             amount       => $fee,
731             description  => $title,
732             note         => undef,
733             user_id      => C4::Context->userenv ? C4::Context->userenv->{'number'} : undef,
734             library_id   => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
735             interface    => C4::Context->interface,
736             invoice_type => undef,
737             type         => 'RESERVE',
738             item_id      => undef
739         }
740     );
741 }
742
743 =head2 GetReserveFee
744
745     $fee = GetReserveFee( $borrowernumber, $biblionumber );
746
747     Calculate the fee for a reserve (if applicable).
748
749 =cut
750
751 sub GetReserveFee {
752     my ( $borrowernumber, $biblionumber ) = @_;
753     my $borquery = qq{
754 SELECT reservefee FROM borrowers LEFT JOIN categories ON borrowers.categorycode = categories.categorycode WHERE borrowernumber = ?
755     };
756     my $issue_qry = qq{
757 SELECT COUNT(*) FROM items
758 LEFT JOIN issues USING (itemnumber)
759 WHERE items.biblionumber=? AND issues.issue_id IS NULL
760     };
761     my $holds_qry = qq{
762 SELECT COUNT(*) FROM reserves WHERE biblionumber=? AND borrowernumber<>?
763     };
764
765     my $dbh = C4::Context->dbh;
766     my ( $fee ) = $dbh->selectrow_array( $borquery, undef, ($borrowernumber) );
767     my $hold_fee_mode = C4::Context->preference('HoldFeeMode') || 'not_always';
768     if( $fee and $fee > 0 and $hold_fee_mode eq 'not_always' ) {
769         # This is a reconstruction of the old code:
770         # Compare number of items with items issued, and optionally check holds
771         # If not all items are issued and there are no holds: charge no fee
772         # NOTE: Lost, damaged, not-for-loan, etc. are just ignored here
773         my ( $notissued, $reserved );
774         ( $notissued ) = $dbh->selectrow_array( $issue_qry, undef,
775             ( $biblionumber ) );
776         if( $notissued == 0 ) {
777             # all items are issued
778             ( $reserved ) = $dbh->selectrow_array( $holds_qry, undef,
779                 ( $biblionumber, $borrowernumber ) );
780             $fee = 0 if $reserved == 0;
781         } else {
782             $fee = 0;
783         }
784     }
785     return $fee;
786 }
787
788 =head2 GetReserveStatus
789
790   $reservestatus = GetReserveStatus($itemnumber);
791
792 Takes an itemnumber and returns the status of the reserve placed on it.
793 If several reserves exist, the reserve with the lower priority is given.
794
795 =cut
796
797 ## FIXME: I don't think this does what it thinks it does.
798 ## It only ever checks the first reserve result, even though
799 ## multiple reserves for that bib can have the itemnumber set
800 ## the sub is only used once in the codebase.
801 sub GetReserveStatus {
802     my ($itemnumber) = @_;
803
804     my $dbh = C4::Context->dbh;
805
806     my ($sth, $found, $priority);
807     if ( $itemnumber ) {
808         $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
809         $sth->execute($itemnumber);
810         ($found, $priority) = $sth->fetchrow_array;
811     }
812
813     if(defined $found) {
814         return 'Waiting'  if $found eq 'W' and $priority == 0;
815         return 'Processing'  if $found eq 'P';
816         return 'Finished' if $found eq 'F';
817     }
818
819     return 'Reserved' if defined $priority && $priority > 0;
820
821     return ''; # empty string here will remove need for checking undef, or less log lines
822 }
823
824 =head2 CheckReserves
825
826   ($status, $matched_reserve, $possible_reserves) = &CheckReserves($itemnumber);
827   ($status, $matched_reserve, $possible_reserves) = &CheckReserves(undef, $barcode);
828   ($status, $matched_reserve, $possible_reserves) = &CheckReserves($itemnumber,undef,$lookahead);
829
830 Find a book in the reserves.
831
832 C<$itemnumber> is the book's item number.
833 C<$lookahead> is the number of days to look in advance for future reserves.
834
835 As I understand it, C<&CheckReserves> looks for the given item in the
836 reserves. If it is found, that's a match, and C<$status> is set to
837 C<Waiting>.
838
839 Otherwise, it finds the most important item in the reserves with the
840 same biblio number as this book (I'm not clear on this) and returns it
841 with C<$status> set to C<Reserved>.
842
843 C<&CheckReserves> returns a two-element list:
844
845 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
846
847 C<$reserve> is the reserve item that matched. It is a
848 reference-to-hash whose keys are mostly the fields of the reserves
849 table in the Koha database.
850
851 =cut
852
853 sub CheckReserves {
854     my ( $item, $barcode, $lookahead_days, $ignore_borrowers) = @_;
855     my $dbh = C4::Context->dbh;
856     my $sth;
857     my $select;
858     if (C4::Context->preference('item-level_itypes')){
859         $select = "
860            SELECT items.biblionumber,
861            items.biblioitemnumber,
862            itemtypes.notforloan,
863            items.notforloan AS itemnotforloan,
864            items.itemnumber,
865            items.damaged,
866            items.homebranch,
867            items.holdingbranch
868            FROM   items
869            LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
870            LEFT JOIN itemtypes   ON items.itype   = itemtypes.itemtype
871         ";
872     }
873     else {
874         $select = "
875            SELECT items.biblionumber,
876            items.biblioitemnumber,
877            itemtypes.notforloan,
878            items.notforloan AS itemnotforloan,
879            items.itemnumber,
880            items.damaged,
881            items.homebranch,
882            items.holdingbranch
883            FROM   items
884            LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
885            LEFT JOIN itemtypes   ON biblioitems.itemtype   = itemtypes.itemtype
886         ";
887     }
888
889     if ($item) {
890         $sth = $dbh->prepare("$select WHERE itemnumber = ?");
891         $sth->execute($item);
892     }
893     else {
894         $sth = $dbh->prepare("$select WHERE barcode = ?");
895         $sth->execute($barcode);
896     }
897     # note: we get the itemnumber because we might have started w/ just the barcode.  Now we know for sure we have it.
898     my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber, $damaged, $item_homebranch, $item_holdingbranch ) = $sth->fetchrow_array;
899     return if ( $damaged && !C4::Context->preference('AllowHoldsOnDamagedItems') );
900
901     return unless $itemnumber; # bail if we got nothing.
902     # if item is not for loan it cannot be reserved either.....
903     # except where items.notforloan < 0 :  This indicates the item is holdable.
904
905     my @SkipHoldTrapOnNotForLoanValue = split( '\|', C4::Context->preference('SkipHoldTrapOnNotForLoanValue') );
906     return if grep { $_ eq $notforloan_per_item } @SkipHoldTrapOnNotForLoanValue;
907
908     my $dont_trap = C4::Context->preference('TrapHoldsOnOrder') ? ($notforloan_per_item > 0) : ($notforloan_per_item && 1 );
909     return if $dont_trap or $notforloan_per_itemtype;
910
911     # Find this item in the reserves
912     my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber, $lookahead_days, $ignore_borrowers);
913
914     # $priority and $highest are used to find the most important item
915     # in the list returned by &_Findgroupreserve. (The lower $priority,
916     # the more important the item.)
917     # $highest is the most important item we've seen so far.
918     my $highest;
919
920     if (scalar @reserves) {
921         my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority');
922         my $LocalHoldsPriorityPatronControl = C4::Context->preference('LocalHoldsPriorityPatronControl');
923         my $LocalHoldsPriorityItemControl = C4::Context->preference('LocalHoldsPriorityItemControl');
924
925         my $priority = 10000000;
926         foreach my $res (@reserves) {
927             if ($res->{'found'} && $res->{'found'} eq 'W') {
928                 return ( "Waiting", $res, \@reserves ); # Found it, it is waiting
929             } elsif ($res->{'found'} && $res->{'found'} eq 'P') {
930                 return ( "Processing", $res, \@reserves ); # Found determinated hold, e. g. the transferred one
931             } elsif ($res->{'found'} && $res->{'found'} eq 'T') {
932                 return ( "Transferred", $res, \@reserves ); # Found determinated hold, e. g. the transferred one
933             } else {
934                 my $patron;
935                 my $item;
936                 my $local_hold_match;
937
938                 if ($LocalHoldsPriority) {
939                     $patron = Koha::Patrons->find( $res->{borrowernumber} );
940                     $item = Koha::Items->find($itemnumber);
941
942                     unless ($item->exclude_from_local_holds_priority || $patron->category->exclude_from_local_holds_priority) {
943                         my $local_holds_priority_item_branchcode =
944                             $item->$LocalHoldsPriorityItemControl;
945                         my $local_holds_priority_patron_branchcode =
946                             ( $LocalHoldsPriorityPatronControl eq 'PickupLibrary' )
947                             ? $res->{branchcode}
948                             : ( $LocalHoldsPriorityPatronControl eq 'HomeLibrary' )
949                             ? $patron->branchcode
950                             : undef;
951                         $local_hold_match =
952                             $local_holds_priority_item_branchcode eq
953                             $local_holds_priority_patron_branchcode;
954                     }
955                 }
956
957                 # See if this item is more important than what we've got so far
958                 if ( ( $res->{'priority'} && $res->{'priority'} < $priority ) || $local_hold_match ) {
959                     $item ||= Koha::Items->find($itemnumber);
960                     next if $res->{itemtype} && $res->{itemtype} ne $item->effective_itemtype;
961                     $patron ||= Koha::Patrons->find( $res->{borrowernumber} );
962                     my $branch = GetReservesControlBranch( $item->unblessed, $patron->unblessed );
963                     my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$item->effective_itemtype);
964                     next if ($branchitemrule->{'holdallowed'} eq 'not_allowed');
965                     next if (($branchitemrule->{'holdallowed'} eq 'from_home_library') && ($item->homebranch ne $patron->branchcode));
966                     my $library = Koha::Libraries->find({branchcode=>$item->homebranch});
967                     next if (($branchitemrule->{'holdallowed'} eq 'from_local_hold_group') && (!$library->validate_hold_sibling({branchcode => $patron->branchcode}) ));
968                     my $hold_fulfillment_policy = $branchitemrule->{hold_fulfillment_policy};
969                     next if ( ($hold_fulfillment_policy eq 'holdgroup') && (!$library->validate_hold_sibling({branchcode => $res->{branchcode}})) );
970                     next if ( ($hold_fulfillment_policy eq 'homebranch') && ($res->{branchcode} ne $item->$hold_fulfillment_policy) );
971                     next if ( ($hold_fulfillment_policy eq 'holdingbranch') && ($res->{branchcode} ne $item->$hold_fulfillment_policy) );
972                     next unless $item->can_be_transferred( { to => Koha::Libraries->find( $res->{branchcode} ) } );
973                     $priority = $res->{'priority'};
974                     $highest  = $res;
975                     last if $local_hold_match;
976                 }
977             }
978         }
979     }
980
981     # If we get this far, then no exact match was found.
982     # We return the most important (i.e. next) reservation.
983     if ($highest) {
984         $highest->{'itemnumber'} = $item;
985         return ( "Reserved", $highest, \@reserves );
986     }
987
988     return ( '' );
989 }
990
991 =head2 CancelExpiredReserves
992
993   CancelExpiredReserves();
994
995 Cancels all reserves with an expiration date from before today.
996
997 =cut
998
999 sub CancelExpiredReserves {
1000     my $cancellation_reason = shift;
1001     my $today = dt_from_string();
1002     my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
1003     my $expireWaiting = C4::Context->preference('ExpireReservesMaxPickUpDelay');
1004
1005     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
1006     my $params = {
1007         -or => [
1008             { expirationdate => { '<', $dtf->format_date($today) } },
1009             { patron_expiration_date => { '<' => $dtf->format_date($today) } }
1010         ]
1011     };
1012
1013     $params->{found} = [ { '!=', 'W' }, undef ]  unless $expireWaiting;
1014
1015     # FIXME To move to Koha::Holds->search_expired (?)
1016     my $holds = Koha::Holds->search( $params );
1017
1018     while ( my $hold = $holds->next ) {
1019         my $calendar = Koha::Calendar->new( branchcode => $hold->branchcode );
1020
1021         next if !$cancel_on_holidays && $calendar->is_holiday( $today );
1022
1023         my $cancel_params = {};
1024         $cancel_params->{cancellation_reason} = $cancellation_reason if defined($cancellation_reason);
1025         if ( defined($hold->found) && $hold->found eq 'W' ) {
1026             $cancel_params->{charge_cancel_fee} = 1;
1027         }
1028         $cancel_params->{autofill} = C4::Context->preference('ExpireReservesAutoFill');
1029         $hold->cancel( $cancel_params );
1030     }
1031 }
1032
1033 =head2 AutoUnsuspendReserves
1034
1035   AutoUnsuspendReserves();
1036
1037 Unsuspends all suspended reserves with a suspend_until date from before today.
1038
1039 =cut
1040
1041 sub AutoUnsuspendReserves {
1042     my $today = dt_from_string();
1043
1044     my @holds = Koha::Holds->search( { suspend_until => { '<=' => $today->ymd() } } )->as_list;
1045
1046     map { $_->resume() } @holds;
1047 }
1048
1049 =head2 ModReserve
1050
1051   ModReserve({ rank => $rank,
1052                reserve_id => $reserve_id,
1053                branchcode => $branchcode
1054                [, itemnumber => $itemnumber ]
1055                [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
1056               });
1057
1058 Change a hold request's priority or cancel it.
1059
1060 C<$rank> specifies the effect of the change.  If C<$rank>
1061 is 'n', nothing happens.  This corresponds to leaving a
1062 request alone when changing its priority in the holds queue
1063 for a bib.
1064
1065 If C<$rank> is 'del', the hold request is cancelled.
1066
1067 If C<$rank> is an integer greater than zero, the priority of
1068 the request is set to that value.  Since priority != 0 means
1069 that the item is not waiting on the hold shelf, setting the
1070 priority to a non-zero value also sets the request's found
1071 status and waiting date to NULL.
1072
1073 If the hold is 'found' (waiting, in-transit, processing) the
1074 only field that can be updated is the expiration date.
1075
1076 The optional C<$itemnumber> parameter is used only when
1077 C<$rank> is a non-zero integer; if supplied, the itemnumber
1078 of the hold request is set accordingly; if omitted, the itemnumber
1079 is cleared.
1080
1081 B<FIXME:> Note that the forgoing can have the effect of causing
1082 item-level hold requests to turn into title-level requests.  This
1083 will be fixed once reserves has separate columns for requested
1084 itemnumber and supplying itemnumber.
1085
1086 =cut
1087
1088 sub ModReserve {
1089     my ( $params ) = @_;
1090
1091     my $rank = $params->{'rank'};
1092     my $reserve_id = $params->{'reserve_id'};
1093     my $branchcode = $params->{'branchcode'};
1094     my $itemnumber = $params->{'itemnumber'};
1095     my $suspend_until = $params->{'suspend_until'};
1096     my $borrowernumber = $params->{'borrowernumber'};
1097     my $biblionumber = $params->{'biblionumber'};
1098     my $cancellation_reason = $params->{'cancellation_reason'};
1099     my $date = $params->{expirationdate};
1100
1101     return if defined $rank && $rank eq "n";
1102
1103     return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
1104
1105     my $hold;
1106     unless ( $reserve_id ) {
1107         my $holds = Koha::Holds->search({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber });
1108         return unless $holds->count; # FIXME Should raise an exception
1109         $hold = $holds->next;
1110         $reserve_id = $hold->reserve_id;
1111     }
1112
1113     $hold ||= Koha::Holds->find($reserve_id);
1114
1115     # FIXME Other calls may fail
1116     Koha::Exceptions::ObjectNotFound->throw( 'No hold with id ' . $reserve_id ) unless $hold;
1117
1118     if ( $rank eq "del" ) {
1119         $hold->cancel({ cancellation_reason => $cancellation_reason });
1120     }
1121     elsif ($hold->found && $hold->priority eq '0' && $date) {
1122         logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, $hold )
1123             if C4::Context->preference('HoldsLog');
1124
1125         # The only column that can be updated for a found hold is the expiration date
1126         $hold->expirationdate($date)->store();
1127     }
1128     elsif ($rank =~ /^\d+/ and $rank > 0) {
1129         logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, $hold )
1130             if C4::Context->preference('HoldsLog');
1131
1132         my $properties = {
1133             priority    => $rank,
1134             branchcode  => $branchcode,
1135             itemnumber  => $itemnumber,
1136             found       => undef,
1137             waitingdate => undef
1138         };
1139         if (exists $params->{reservedate}) {
1140             $properties->{reservedate} = $params->{reservedate} || undef;
1141         }
1142         if (exists $params->{expirationdate}) {
1143             $properties->{expirationdate} = $params->{expirationdate} || undef;
1144         }
1145
1146         $hold->set($properties)->store();
1147
1148         if ( defined( $suspend_until ) ) {
1149             if ( $suspend_until ) {
1150                 $hold->suspend_hold( $suspend_until );
1151             } else {
1152                 # If the hold is suspended leave the hold suspended, but convert it to an indefinite hold.
1153                 # If the hold is not suspended, this does nothing.
1154                 $hold->set( { suspend_until => undef } )->store();
1155             }
1156         }
1157
1158         _FixPriority({ reserve_id => $reserve_id, rank =>$rank });
1159     }
1160 }
1161
1162 =head2 ModReserveStatus
1163
1164   &ModReserveStatus($itemnumber, $newstatus);
1165
1166 Update the reserve status for the active (priority=0) reserve.
1167
1168 $itemnumber is the itemnumber the reserve is on
1169
1170 $newstatus is the new status.
1171
1172 =cut
1173
1174 sub ModReserveStatus {
1175
1176     #first : check if we have a reservation for this item .
1177     my ($itemnumber, $newstatus) = @_;
1178     my $dbh = C4::Context->dbh;
1179
1180     my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1181     my $sth_set = $dbh->prepare($query);
1182     $sth_set->execute( $newstatus, $itemnumber );
1183
1184     my $item = Koha::Items->find($itemnumber);
1185     if ( $item->location && $item->location eq 'CART'
1186         && ( !$item->permanent_location || $item->permanent_location ne 'CART' )
1187         && $newstatus ) {
1188       CartToShelf( $itemnumber );
1189     }
1190 }
1191
1192 =head2 ModReserveAffect
1193
1194   &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend,$reserve_id, $desk_id, $notify_library);
1195
1196 This function affect an item and a status for a given reserve, either fetched directly
1197 by record_id, or by borrowernumber and itemnumber or biblionumber. If only biblionumber
1198 is given, only first reserve returned is affected, which is ok for anything but
1199 multi-item holds.
1200
1201 if $transferToDo is not set, then the status is set to "Waiting" as well.
1202 otherwise, a transfer is on the way, and the end of the transfer will
1203 take care of the waiting status
1204
1205 This function also removes any entry of the hold in holds queue table.
1206
1207 =cut
1208
1209 sub ModReserveAffect {
1210     my ( $itemnumber, $borrowernumber, $transferToDo, $reserve_id, $desk_id, $notify_library ) = @_;
1211     my $dbh = C4::Context->dbh;
1212
1213     # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1214     # attached to $itemnumber
1215     my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1216     $sth->execute($itemnumber);
1217     my ($biblionumber) = $sth->fetchrow;
1218
1219     # get request - need to find out if item is already
1220     # waiting in order to not send duplicate hold filled notifications
1221
1222     my $hold;
1223     # Find hold by id if we have it
1224     $hold = Koha::Holds->find( $reserve_id ) if $reserve_id;
1225     # Find item level hold for this item if there is one
1226     $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->next();
1227     # Find record level hold if there is no item level hold
1228     $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->next();
1229
1230     return unless $hold;
1231
1232     my $already_on_shelf = $hold->found && $hold->found eq 'W';
1233
1234     $hold->itemnumber($itemnumber);
1235
1236     if ($transferToDo) {
1237         $hold->set_transfer();
1238     } elsif (C4::Context->preference('HoldsNeedProcessingSIP')
1239              && C4::Context->interface eq 'sip'
1240              && !$already_on_shelf) {
1241         $hold->set_processing();
1242     } else {
1243         $hold->set_waiting($desk_id);
1244         _koha_notify_reserve( $hold->reserve_id ) unless $already_on_shelf;
1245         # Complete transfer if one exists
1246         my $transfer = $hold->item->get_transfer;
1247         $transfer->receive if $transfer;
1248     }
1249
1250     _koha_notify_hold_changed( $hold ) if $notify_library;
1251
1252     _FixPriority( { biblionumber => $biblionumber } );
1253     my $item = Koha::Items->find($itemnumber);
1254     if ( $item->location && $item->location eq 'CART'
1255         && ( !$item->permanent_location || $item->permanent_location ne 'CART' ) ) {
1256       CartToShelf( $itemnumber );
1257     }
1258
1259     my $std = $dbh->prepare(q{
1260         DELETE  q, t
1261         FROM    tmp_holdsqueue q
1262         INNER JOIN hold_fill_targets t
1263         ON  q.borrowernumber = t.borrowernumber
1264             AND q.biblionumber = t.biblionumber
1265             AND q.itemnumber = t.itemnumber
1266             AND q.item_level_request = t.item_level_request
1267             AND q.holdingbranch = t.source_branchcode
1268         WHERE t.reserve_id = ?
1269     });
1270     $std->execute($hold->reserve_id);
1271
1272     logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, $hold )
1273         if C4::Context->preference('HoldsLog');
1274
1275     return;
1276 }
1277
1278 =head2 ModReserveCancelAll
1279
1280   ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber,$reason);
1281
1282 function to cancel reserv,check other reserves, and transfer document if it's necessary
1283
1284 =cut
1285
1286 sub ModReserveCancelAll {
1287     my $messages;
1288     my $nextreservinfo;
1289     my ( $itemnumber, $borrowernumber, $cancellation_reason ) = @_;
1290
1291     #step 1 : cancel the reservation
1292     my $holds = Koha::Holds->search({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1293     return unless $holds->count;
1294     $holds->next->cancel({ cancellation_reason => $cancellation_reason });
1295
1296     #step 2 launch the subroutine of the others reserves
1297     ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1298
1299     return ( $messages, $nextreservinfo->{borrowernumber} );
1300 }
1301
1302 =head2 ModReserveMinusPriority
1303
1304   &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1305
1306 Reduce the values of queued list
1307
1308 =cut
1309
1310 sub ModReserveMinusPriority {
1311     my ( $itemnumber, $reserve_id ) = @_;
1312
1313     #first step update the value of the first person on reserv
1314     my $dbh   = C4::Context->dbh;
1315     my $query = "
1316         UPDATE reserves
1317         SET    priority = 0 , itemnumber = ?
1318         WHERE  reserve_id = ?
1319     ";
1320     my $sth_upd = $dbh->prepare($query);
1321     $sth_upd->execute( $itemnumber, $reserve_id );
1322     # second step update all others reserves
1323     _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1324 }
1325
1326 =head2 IsAvailableForItemLevelRequest
1327
1328   my $is_available = IsAvailableForItemLevelRequest( $item_record, $borrower_record, $pickup_branchcode );
1329
1330 Checks whether a given item record is available for an
1331 item-level hold request.  An item is available if
1332
1333 * it is not lost AND
1334 * it is not damaged AND
1335 * it is not withdrawn AND
1336 * a waiting or in transit reserve is placed on
1337 * does not have a not for loan value > 0
1338
1339 Need to check the issuingrules onshelfholds column,
1340 if this is set items on the shelf can be placed on hold
1341
1342 Note that IsAvailableForItemLevelRequest() does not
1343 check if the staff operator is authorized to place
1344 a request on the item - in particular,
1345 this routine does not check IndependentBranches
1346 and canreservefromotherbranches.
1347
1348 Note also that this subroutine does not checks smart
1349 rules limits for item by reservesallowed/holds_per_record
1350 values, this complemented in calling code with calls and
1351 checks with CanItemBeReserved or CanBookBeReserved.
1352
1353 =cut
1354
1355 sub IsAvailableForItemLevelRequest {
1356     my $item                = shift;
1357     my $patron              = shift;
1358     my $pickup_branchcode   = shift;
1359     # items_any_available is precalculated status passed from request.pl when set of items
1360     # looped outside of IsAvailableForItemLevelRequest to avoid nested loops:
1361     my $items_any_available = shift;
1362
1363     my $dbh = C4::Context->dbh;
1364     # must check the notforloan setting of the itemtype
1365     # FIXME - a lot of places in the code do this
1366     #         or something similar - need to be
1367     #         consolidated
1368     my $itemtype = $item->effective_itemtype;
1369     return 0
1370       unless defined $itemtype;
1371     my $notforloan_per_itemtype = Koha::ItemTypes->find($itemtype)->notforloan;
1372
1373     return 0 if
1374         $notforloan_per_itemtype ||
1375         $item->itemlost        ||
1376         $item->notforloan > 0  || # item with negative or zero notforloan value is holdable
1377         $item->withdrawn        ||
1378         ($item->damaged && !C4::Context->preference('AllowHoldsOnDamagedItems'));
1379
1380     if ($pickup_branchcode) {
1381         my $destination = Koha::Libraries->find($pickup_branchcode);
1382         return 0 unless $destination;
1383         return 0 unless $destination->pickup_location;
1384         return 0 unless $item->can_be_transferred( { to => $destination } );
1385         my $reserves_control_branch =
1386             GetReservesControlBranch( $item->unblessed(), $patron->unblessed() );
1387         my $branchitemrule =
1388             C4::Circulation::GetBranchItemRule( $reserves_control_branch, $item->itype );
1389         my $home_library = Koha::Libraries->find( {branchcode => $item->homebranch} );
1390         return 0 unless $branchitemrule->{hold_fulfillment_policy} ne 'holdgroup' || $home_library->validate_hold_sibling( {branchcode => $pickup_branchcode} );
1391     }
1392
1393     my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy( { item => $item, patron => $patron } );
1394
1395     if ( $on_shelf_holds == 1 ) {
1396         return 1;
1397     } elsif ( $on_shelf_holds == 2 ) {
1398
1399         # if we have this param predefined from outer caller sub, we just need
1400         # to return it, so we saving from having loop inside other loop:
1401         return  $items_any_available ? 0 : 1
1402             if defined $items_any_available;
1403
1404         my $any_available = ItemsAnyAvailableAndNotRestricted( { biblionumber => $item->biblionumber, patron => $patron });
1405         return $any_available ? 0 : 1;
1406     } else { # on_shelf_holds == 0 "If any unavailable" (the description is rather cryptic and could still be improved)
1407         return $item->onloan || IsItemOnHoldAndFound( $item->itemnumber );
1408     }
1409 }
1410
1411 =head2 ItemsAnyAvailableAndNotRestricted
1412
1413   ItemsAnyAvailableAndNotRestricted( { biblionumber => $biblionumber, patron => $patron });
1414
1415 This function checks all items for specified biblionumber (numeric) against patron (object)
1416 and returns true (1) if at least one item available for loan/check out/present/not held
1417 and also checks other parameters logic which not restricts item for hold at all (for ex.
1418 AllowHoldsOnDamagedItems or 'holdallowed' own/sibling library)
1419
1420 =cut
1421
1422 sub ItemsAnyAvailableAndNotRestricted {
1423     my $param = shift;
1424
1425     my @items = Koha::Items->search( { biblionumber => $param->{biblionumber} } )->as_list;
1426
1427     foreach my $i (@items) {
1428         my $reserves_control_branch =
1429             GetReservesControlBranch( $i->unblessed(), $param->{patron}->unblessed );
1430         my $branchitemrule =
1431             C4::Circulation::GetBranchItemRule( $reserves_control_branch, $i->itype );
1432         my $item_library = Koha::Libraries->find( { branchcode => $i->homebranch } );
1433
1434         # we can return (end the loop) when first one found:
1435         return 1
1436             unless $i->itemlost
1437             || $i->notforloan # items with non-zero notforloan cannot be checked out
1438             || $i->withdrawn
1439             || $i->onloan
1440             || IsItemOnHoldAndFound( $i->id )
1441             || ( $i->damaged
1442                  && ! C4::Context->preference('AllowHoldsOnDamagedItems') )
1443             || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan
1444             || $branchitemrule->{holdallowed} eq 'from_home_library' && $param->{patron}->branchcode ne $i->homebranch
1445             || $branchitemrule->{holdallowed} eq 'from_local_hold_group' && ! $item_library->validate_hold_sibling( { branchcode => $param->{patron}->branchcode } )
1446             || CanItemBeReserved( $param->{patron}, $i )->{status} ne 'OK';
1447     }
1448
1449     return 0;
1450 }
1451
1452 =head2 AlterPriority
1453
1454   AlterPriority( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority );
1455
1456 This function changes a reserve's priority up, down, to the top, or to the bottom.
1457 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1458
1459 =cut
1460
1461 sub AlterPriority {
1462     my ( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority ) = @_;
1463
1464     my $hold = Koha::Holds->find( $reserve_id );
1465     return unless $hold;
1466
1467     if ( $hold->cancellationdate ) {
1468         warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (" . $hold->cancellationdate . ')';
1469         return;
1470     }
1471
1472     if ( $where eq 'up' ) {
1473       return unless $prev_priority;
1474       _FixPriority({ reserve_id => $reserve_id, rank => $prev_priority })
1475     } elsif ( $where eq 'down' ) {
1476       return unless $next_priority;
1477       _FixPriority({ reserve_id => $reserve_id, rank => $next_priority })
1478     } elsif ( $where eq 'top' ) {
1479       _FixPriority({ reserve_id => $reserve_id, rank => $first_priority })
1480     } elsif ( $where eq 'bottom' ) {
1481       _FixPriority({ reserve_id => $reserve_id, rank => $last_priority });
1482     }
1483
1484     Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
1485         {
1486             biblio_ids => [ $hold->biblionumber ]
1487         }
1488     ) if C4::Context->preference('RealTimeHoldsQueue');
1489     # FIXME Should return the new priority
1490 }
1491
1492 =head2 ToggleLowestPriority
1493
1494   ToggleLowestPriority( $borrowernumber, $biblionumber );
1495
1496 This function sets the lowestPriority field to true if is false, and false if it is true.
1497
1498 =cut
1499
1500 sub ToggleLowestPriority {
1501     my ( $reserve_id ) = @_;
1502
1503     my $dbh = C4::Context->dbh;
1504
1505     my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1506     $sth->execute( $reserve_id );
1507
1508     _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1509 }
1510
1511 =head2 ToggleSuspend
1512
1513   ToggleSuspend( $reserve_id );
1514
1515 This function sets the suspend field to true if is false, and false if it is true.
1516 If the reserve is currently suspended with a suspend_until date, that date will
1517 be cleared when it is unsuspended.
1518
1519 =cut
1520
1521 sub ToggleSuspend {
1522     my ( $reserve_id, $suspend_until ) = @_;
1523
1524     my $hold = Koha::Holds->find( $reserve_id );
1525
1526     if ( $hold->is_suspended ) {
1527         $hold->resume()
1528     } else {
1529         $hold->suspend_hold( $suspend_until );
1530     }
1531 }
1532
1533 =head2 SuspendAll
1534
1535   SuspendAll(
1536       borrowernumber   => $borrowernumber,
1537       [ biblionumber   => $biblionumber, ]
1538       [ suspend_until  => $suspend_until, ]
1539       [ suspend        => $suspend ]
1540   );
1541
1542   This function accepts a set of hash keys as its parameters.
1543   It requires either borrowernumber or biblionumber, or both.
1544
1545   suspend_until is wholly optional.
1546
1547 =cut
1548
1549 sub SuspendAll {
1550     my %params = @_;
1551
1552     my $borrowernumber = $params{'borrowernumber'} || undef;
1553     my $biblionumber   = $params{'biblionumber'}   || undef;
1554     my $suspend_until  = $params{'suspend_until'}  || undef;
1555     my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1556
1557     return unless ( $borrowernumber || $biblionumber );
1558
1559     my $params;
1560     $params->{found}          = undef;
1561     $params->{borrowernumber} = $borrowernumber if $borrowernumber;
1562     $params->{biblionumber}   = $biblionumber if $biblionumber;
1563
1564     my @holds = Koha::Holds->search($params)->as_list;
1565
1566     if ($suspend) {
1567         map { $_->suspend_hold($suspend_until) } @holds;
1568     }
1569     else {
1570         map { $_->resume() } @holds;
1571     }
1572 }
1573
1574
1575 =head2 _FixPriority
1576
1577   _FixPriority({
1578     reserve_id => $reserve_id,
1579     [rank => $rank,]
1580     [ignoreSetLowestRank => $ignoreSetLowestRank]
1581   });
1582
1583   or
1584
1585   _FixPriority({ biblionumber => $biblionumber});
1586
1587 This routine adjusts the priority of a hold request and holds
1588 on the same bib.
1589
1590 In the first form, where a reserve_id is passed, the priority of the
1591 hold is set to supplied rank, and other holds for that bib are adjusted
1592 accordingly.  If the rank is "del", the hold is cancelled.  If no rank
1593 is supplied, all of the holds on that bib have their priority adjusted
1594 as if the second form had been used.
1595
1596 In the second form, where a biblionumber is passed, the holds on that
1597 bib (that are not captured) are sorted in order of increasing priority,
1598 then have reserves.priority set so that the first non-captured hold
1599 has its priority set to 1, the second non-captured hold has its priority
1600 set to 2, and so forth.
1601
1602 In both cases, holds that have the lowestPriority flag on are have their
1603 priority adjusted to ensure that they remain at the end of the line.
1604
1605 Note that the ignoreSetLowestRank parameter is meant to be used only
1606 when _FixPriority calls itself.
1607
1608 =cut
1609
1610 sub _FixPriority {
1611     my ( $params ) = @_;
1612     my $reserve_id = $params->{reserve_id};
1613     my $rank = $params->{rank} // '';
1614     my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1615     my $biblionumber = $params->{biblionumber};
1616
1617     my $dbh = C4::Context->dbh;
1618
1619     my $hold;
1620     if ( $reserve_id ) {
1621         $hold = Koha::Holds->find( $reserve_id );
1622         if (!defined $hold){
1623             # may have already been checked out and hold fulfilled
1624             $hold = Koha::Old::Holds->find( $reserve_id );
1625         }
1626         return unless $hold;
1627     }
1628
1629     unless ( $biblionumber ) { # FIXME This is a very weird API
1630         $biblionumber = $hold->biblionumber;
1631     }
1632
1633     if ( $rank eq "del" ) { # FIXME will crash if called without $hold
1634         $hold->cancel;
1635     }
1636     elsif ( $reserve_id && ( $rank eq "W" || $rank eq "0" ) ) {
1637
1638         # make sure priority for waiting or in-transit items is 0
1639         my $query = "
1640             UPDATE reserves
1641             SET    priority = 0
1642             WHERE reserve_id = ?
1643             AND found IN ('W', 'T', 'P')
1644         ";
1645         my $sth = $dbh->prepare($query);
1646         $sth->execute( $reserve_id );
1647     }
1648     my @priority;
1649
1650     # get whats left
1651     my $query = "
1652         SELECT reserve_id, borrowernumber, reservedate
1653         FROM   reserves
1654         WHERE  biblionumber   = ?
1655           AND  ((found <> 'W' AND found <> 'T' AND found <> 'P') OR found IS NULL)
1656         ORDER BY priority ASC
1657     ";
1658     my $sth = $dbh->prepare($query);
1659     $sth->execute( $biblionumber );
1660     while ( my $line = $sth->fetchrow_hashref ) {
1661         push( @priority,     $line );
1662     }
1663
1664     # FIXME This whole sub must be rewritten, especially to highlight what is done when reserve_id is not given
1665     # To find the matching index
1666     my $i;
1667     my $key = -1;    # to allow for 0 to be a valid result
1668     for ( $i = 0 ; $i < @priority ; $i++ ) {
1669         if ( $reserve_id && $reserve_id == $priority[$i]->{'reserve_id'} ) {
1670             $key = $i;    # save the index
1671             last;
1672         }
1673     }
1674
1675     # if index exists in array then move it to new position
1676     if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1677         my $new_rank = $rank - 1; # $new_rank is what you want the new index to be in the array
1678         my $moving_item = splice( @priority, $key, 1 );
1679         $new_rank = scalar @priority if $new_rank > scalar @priority;
1680         splice( @priority, $new_rank, 0, $moving_item );
1681     }
1682
1683     # now fix the priority on those that are left....
1684     $query = "
1685         UPDATE reserves
1686         SET    priority = ?
1687         WHERE  reserve_id = ?
1688     ";
1689     $sth = $dbh->prepare($query);
1690     for ( my $j = 0 ; $j < @priority ; $j++ ) {
1691         $sth->execute(
1692             $j + 1,
1693             $priority[$j]->{'reserve_id'}
1694         );
1695     }
1696
1697     unless ( $ignoreSetLowestRank ) {
1698         $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 AND biblionumber = ? ORDER BY priority" );
1699         $sth->execute($biblionumber);
1700       while ( my $res = $sth->fetchrow_hashref() ) {
1701         _FixPriority({
1702             reserve_id => $res->{'reserve_id'},
1703             rank => '999999',
1704             ignoreSetLowestRank => 1
1705         });
1706       }
1707     }
1708 }
1709
1710 =head2 _Findgroupreserve
1711
1712   @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead, $ignore_borrowers);
1713
1714 Looks for a holds-queue based item-specific match first, then for a holds-queue title-level match, returning the
1715 first match found.  If neither, then we look for non-holds-queue based holds.
1716 Lookahead is the number of days to look in advance.
1717
1718 C<&_Findgroupreserve> returns :
1719 C<@results> is an array of references-to-hash whose keys are mostly
1720 fields from the reserves table of the Koha database, plus
1721 C<biblioitemnumber>.
1722
1723 This routine with either return:
1724 1 - Item specific holds from the holds queue
1725 2 - Title level holds from the holds queue
1726 3 - All holds for this biblionumber
1727
1728 All return values will respect any borrowernumbers passed as arrayref in $ignore_borrowers
1729
1730 =cut
1731
1732 sub _Findgroupreserve {
1733     my ( $bibitem, $biblio, $itemnumber, $lookahead, $ignore_borrowers) = @_;
1734     my $dbh   = C4::Context->dbh;
1735
1736     # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1737     # check for exact targeted match
1738     my $item_level_target_query = qq{
1739         SELECT reserves.biblionumber        AS biblionumber,
1740                reserves.borrowernumber      AS borrowernumber,
1741                reserves.reservedate         AS reservedate,
1742                reserves.branchcode          AS branchcode,
1743                reserves.cancellationdate    AS cancellationdate,
1744                reserves.found               AS found,
1745                reserves.reservenotes        AS reservenotes,
1746                reserves.priority            AS priority,
1747                reserves.timestamp           AS timestamp,
1748                biblioitems.biblioitemnumber AS biblioitemnumber,
1749                reserves.itemnumber          AS itemnumber,
1750                reserves.reserve_id          AS reserve_id,
1751                reserves.itemtype            AS itemtype,
1752                reserves.non_priority        AS non_priority
1753         FROM reserves
1754         JOIN biblioitems USING (biblionumber)
1755         JOIN hold_fill_targets USING (reserve_id)
1756         WHERE found IS NULL
1757         AND priority > 0
1758         AND item_level_request = 1
1759         AND hold_fill_targets.itemnumber = ?
1760         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1761         AND suspend = 0
1762         ORDER BY priority
1763     };
1764     my $sth = $dbh->prepare($item_level_target_query);
1765     $sth->execute($itemnumber, $lookahead||0);
1766     my @results;
1767     if ( my $data = $sth->fetchrow_hashref ) {
1768         push( @results, $data )
1769           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1770     }
1771     return @results if @results;
1772
1773     # check for title-level targeted match
1774     my $title_level_target_query = qq{
1775         SELECT reserves.biblionumber        AS biblionumber,
1776                reserves.borrowernumber      AS borrowernumber,
1777                reserves.reservedate         AS reservedate,
1778                reserves.branchcode          AS branchcode,
1779                reserves.cancellationdate    AS cancellationdate,
1780                reserves.found               AS found,
1781                reserves.reservenotes        AS reservenotes,
1782                reserves.priority            AS priority,
1783                reserves.timestamp           AS timestamp,
1784                biblioitems.biblioitemnumber AS biblioitemnumber,
1785                reserves.itemnumber          AS itemnumber,
1786                reserves.reserve_id          AS reserve_id,
1787                reserves.itemtype            AS itemtype,
1788                reserves.non_priority        AS non_priority
1789         FROM reserves
1790         JOIN biblioitems USING (biblionumber)
1791         JOIN hold_fill_targets USING (reserve_id)
1792         WHERE found IS NULL
1793         AND priority > 0
1794         AND item_level_request = 0
1795         AND hold_fill_targets.itemnumber = ?
1796         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1797         AND suspend = 0
1798         ORDER BY priority
1799     };
1800     $sth = $dbh->prepare($title_level_target_query);
1801     $sth->execute($itemnumber, $lookahead||0);
1802     @results = ();
1803     if ( my $data = $sth->fetchrow_hashref ) {
1804         push( @results, $data )
1805           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1806     }
1807     return @results if @results;
1808
1809     my $query = qq{
1810         SELECT reserves.biblionumber               AS biblionumber,
1811                reserves.borrowernumber             AS borrowernumber,
1812                reserves.reservedate                AS reservedate,
1813                reserves.waitingdate                AS waitingdate,
1814                reserves.branchcode                 AS branchcode,
1815                reserves.cancellationdate           AS cancellationdate,
1816                reserves.found                      AS found,
1817                reserves.reservenotes               AS reservenotes,
1818                reserves.priority                   AS priority,
1819                reserves.timestamp                  AS timestamp,
1820                reserves.itemnumber                 AS itemnumber,
1821                reserves.reserve_id                 AS reserve_id,
1822                reserves.itemtype                   AS itemtype,
1823                reserves.non_priority        AS non_priority
1824         FROM reserves
1825         WHERE reserves.biblionumber = ?
1826           AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1827           AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1828           AND suspend = 0
1829           ORDER BY priority
1830     };
1831     $sth = $dbh->prepare($query);
1832     $sth->execute( $biblio, $itemnumber, $lookahead||0);
1833     @results = ();
1834     while ( my $data = $sth->fetchrow_hashref ) {
1835         push( @results, $data )
1836           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1837     }
1838     return @results;
1839 }
1840
1841 =head2 _koha_notify_reserve
1842
1843   _koha_notify_reserve( $hold->reserve_id );
1844
1845 Sends a notification to the patron that their hold has been filled (through
1846 ModReserveAffect)
1847
1848 The letter code for this notice may be found using the following query:
1849
1850     select distinct letter_code
1851     from message_transports
1852     inner join message_attributes using (message_attribute_id)
1853     where message_name = 'Hold_Filled'
1854
1855 This will probably sipmly be 'HOLD', but because it is defined in the database,
1856 it is subject to addition or change.
1857
1858 The following tables are availalbe witin the notice:
1859
1860     branches
1861     borrowers
1862     biblio
1863     biblioitems
1864     reserves
1865     items
1866
1867 =cut
1868
1869 sub _koha_notify_reserve {
1870     my $reserve_id = shift;
1871
1872     my $hold = Koha::Holds->find($reserve_id);
1873     my $borrowernumber = $hold->borrowernumber;
1874
1875     my $patron = Koha::Patrons->find( $borrowernumber );
1876
1877     # Try to get the borrower's email address
1878     my $to_address = $patron->notice_email_address;
1879
1880     my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
1881             borrowernumber => $borrowernumber,
1882             message_name => 'Hold_Filled'
1883     } );
1884
1885     my $library = Koha::Libraries->find( $hold->branchcode );
1886     my $inbound_email_address = $library->inbound_email_address;
1887
1888     my %letter_params = (
1889         module => 'reserves',
1890         branchcode => $hold->branchcode,
1891         lang => $patron->lang,
1892         tables => {
1893             'branches'       => $library->unblessed,
1894             'borrowers'      => $patron->unblessed,
1895             'biblio'         => $hold->biblionumber,
1896             'biblioitems'    => $hold->biblionumber,
1897             'reserves'       => $hold->unblessed,
1898             'items'          => $hold->itemnumber,
1899         },
1900     );
1901
1902     my $notification_sent = 0; #Keeping track if a Hold_filled message is sent. If no message can be sent, then default to a print message.
1903     my $send_notification = sub {
1904         my ( $mtt, $letter_code ) = (@_);
1905         return unless defined $letter_code;
1906         $letter_params{letter_code} = $letter_code;
1907         $letter_params{message_transport_type} = $mtt;
1908         my $letter =  C4::Letters::GetPreparedLetter ( %letter_params );
1909         unless ($letter) {
1910             warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
1911             return;
1912         }
1913
1914         C4::Letters::EnqueueLetter( {
1915             letter => $letter,
1916             borrowernumber => $borrowernumber,
1917             from_address => $inbound_email_address,
1918             message_transport_type => $mtt,
1919         } );
1920     };
1921
1922     while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
1923         next if (
1924                ( $mtt eq 'email' and not $to_address ) # No email address
1925             or ( $mtt eq 'sms'   and not $patron->smsalertnumber ) # No SMS number
1926             or ( $mtt eq 'itiva' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl
1927             or ( $mtt eq 'phone' and not $patron->phone ) # No phone number to call
1928         );
1929
1930         &$send_notification($mtt, $letter_code);
1931         $notification_sent++;
1932     }
1933     #Making sure that a print notification is sent if no other transport types can be utilized.
1934     if (! $notification_sent) {
1935         &$send_notification('print', 'HOLD');
1936     }
1937
1938 }
1939
1940 =head2 _koha_notify_hold_changed
1941
1942   _koha_notify_hold_changed( $hold_object );
1943
1944 =cut
1945
1946 sub _koha_notify_hold_changed {
1947     my $hold = shift;
1948
1949     my $patron = $hold->patron;
1950     my $library = $hold->branch;
1951
1952     my $letter = C4::Letters::GetPreparedLetter(
1953         module      => 'reserves',
1954         letter_code => 'HOLD_CHANGED',
1955         branchcode  => $hold->branchcode,
1956         substitute  => { today => output_pref( dt_from_string ) },
1957         tables      => {
1958             'branches'    => $library->unblessed,
1959             'borrowers'   => $patron->unblessed,
1960             'biblio'      => $hold->biblionumber,
1961             'biblioitems' => $hold->biblionumber,
1962             'reserves'    => $hold->unblessed,
1963             'items'       => $hold->itemnumber,
1964         },
1965     );
1966
1967     return unless $letter;
1968
1969     my $email =
1970          C4::Context->preference('ExpireReservesAutoFillEmail')
1971       || $library->inbound_email_address;
1972
1973     C4::Letters::EnqueueLetter(
1974         {
1975             letter                 => $letter,
1976             borrowernumber         => $patron->id,
1977             message_transport_type => 'email',
1978             from_address           => $email,
1979             to_address             => $email,
1980         }
1981     );
1982 }
1983
1984 =head2 _ShiftPriority
1985
1986   $new_priority = _ShiftPriority( $biblionumber, $priority );
1987
1988 This increments the priority of all reserves after the one
1989 with either the lowest date after C<$reservedate>
1990 or the lowest priority after C<$priority>.
1991
1992 It effectively makes room for a new reserve to be inserted with a certain
1993 priority, which is returned.
1994
1995 This is most useful when the reservedate can be set by the user.  It allows
1996 the new reserve to be placed before other reserves that have a later
1997 reservedate.  Since priority also is set by the form in reserves/request.pl
1998 the sub accounts for that too.
1999
2000 =cut
2001
2002 sub _ShiftPriority {
2003     my ( $biblio, $new_priority ) = @_;
2004
2005     my $dbh = C4::Context->dbh;
2006     my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND priority > ? ORDER BY priority ASC LIMIT 1";
2007     my $sth = $dbh->prepare( $query );
2008     $sth->execute( $biblio, $new_priority );
2009     my $min_priority = $sth->fetchrow;
2010     # if no such matches are found, $new_priority remains as original value
2011     $new_priority = $min_priority if ( $min_priority );
2012
2013     # Shift the priority up by one; works in conjunction with the next SQL statement
2014     $query = "UPDATE reserves
2015               SET priority = priority+1
2016               WHERE biblionumber = ?
2017               AND borrowernumber = ?
2018               AND reservedate = ?
2019               AND found IS NULL";
2020     my $sth_update = $dbh->prepare( $query );
2021
2022     # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
2023     $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
2024     $sth = $dbh->prepare( $query );
2025     $sth->execute( $new_priority, $biblio );
2026     while ( my $row = $sth->fetchrow_hashref ) {
2027         $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
2028     }
2029
2030     return $new_priority;  # so the caller knows what priority they wind up receiving
2031 }
2032
2033 =head2 MoveReserve
2034
2035   MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
2036
2037 Use when checking out an item to handle reserves
2038 If $cancelreserve boolean is set to true, it will remove existing reserve
2039
2040 =cut
2041
2042 sub MoveReserve {
2043     my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
2044
2045     $cancelreserve //= 0;
2046
2047     my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
2048     my ( $restype, $res, undef ) = CheckReserves( $itemnumber, undef, $lookahead );
2049     return unless $res;
2050
2051     my $biblionumber = $res->{biblionumber};
2052
2053     if ($res->{borrowernumber} == $borrowernumber) {
2054         my $hold = Koha::Holds->find( $res->{reserve_id} );
2055         $hold->fill;
2056     }
2057     else {
2058         # warn "Reserved";
2059         # The item is reserved by someone else.
2060         # Find this item in the reserves
2061
2062         my $borr_res  = Koha::Holds->search({
2063             borrowernumber => $borrowernumber,
2064             biblionumber   => $biblionumber,
2065         },{
2066             order_by       => 'priority'
2067         })->next();
2068
2069         if ( $borr_res ) {
2070             # The item is reserved by the current patron
2071             $borr_res->fill;
2072         }
2073
2074         if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
2075             RevertWaitingStatus({ itemnumber => $itemnumber });
2076         }
2077         elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
2078             my $hold = Koha::Holds->find( $res->{reserve_id} );
2079             $hold->cancel;
2080         }
2081     }
2082 }
2083
2084 =head2 MergeHolds
2085
2086   MergeHolds($dbh,$to_biblio, $from_biblio);
2087
2088 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
2089
2090 =cut
2091
2092 sub MergeHolds {
2093     my ( $dbh, $to_biblio, $from_biblio ) = @_;
2094     my $sth = $dbh->prepare(
2095         "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
2096     );
2097     $sth->execute($from_biblio);
2098     if ( my $data = $sth->fetchrow_hashref() ) {
2099
2100         # holds exist on old record, if not we don't need to do anything
2101         $sth = $dbh->prepare(
2102             "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
2103         $sth->execute( $to_biblio, $from_biblio );
2104
2105         # Reorder by date
2106         # don't reorder those already waiting
2107
2108         $sth = $dbh->prepare(
2109 "SELECT * FROM reserves WHERE biblionumber = ? AND (found NOT IN ('W', 'T', 'P') OR found is NULL) ORDER BY reservedate ASC"
2110         );
2111         my $upd_sth = $dbh->prepare(
2112 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
2113         AND reservedate = ? AND (itemnumber = ? or itemnumber is NULL) "
2114         );
2115         $sth->execute( $to_biblio );
2116         my $priority = 1;
2117         while ( my $reserve = $sth->fetchrow_hashref() ) {
2118             $upd_sth->execute(
2119                 $priority,                    $to_biblio,
2120                 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
2121                 $reserve->{'itemnumber'}
2122             );
2123             $priority++;
2124         }
2125     }
2126 }
2127
2128 =head2 RevertWaitingStatus
2129
2130   RevertWaitingStatus({ itemnumber => $itemnumber });
2131
2132   Reverts a 'waiting' hold back to a regular hold with a priority of 1.
2133
2134   Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
2135           item level hold, even if it was only a bibliolevel hold to
2136           begin with. This is because we can no longer know if a hold
2137           was item-level or bib-level after a hold has been set to
2138           waiting status.
2139
2140 =cut
2141
2142 sub RevertWaitingStatus {
2143     my ( $params ) = @_;
2144     my $itemnumber = $params->{'itemnumber'};
2145
2146     return unless ( $itemnumber );
2147
2148     my $dbh = C4::Context->dbh;
2149
2150     ## Get the waiting reserve we want to revert
2151     my $hold = Koha::Holds->search(
2152         {
2153             itemnumber => $itemnumber,
2154             found => { not => undef },
2155         }
2156     )->next;
2157
2158     ## Increment the priority of all other non-waiting
2159     ## reserves for this bib record
2160     my $holds = Koha::Holds->search({ biblionumber => $hold->biblionumber, priority => { '>' => 0 } })
2161                            ->update({ priority => \'priority + 1' }, { no_triggers => 1 });
2162
2163     ## Fix up the currently waiting reserve
2164     $hold->set(
2165         {
2166             priority    => 1,
2167             found       => undef,
2168             waitingdate => undef,
2169             expirationdate => $hold->patron_expiration_date,
2170             itemnumber  => $hold->item_level_hold ? $hold->itemnumber : undef,
2171         }
2172     )->store();
2173
2174     _FixPriority( { biblionumber => $hold->biblionumber } );
2175
2176     Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue(
2177         {
2178             biblio_ids => [ $hold->biblionumber ]
2179         }
2180     ) if C4::Context->preference('RealTimeHoldsQueue');
2181
2182
2183     return $hold;
2184 }
2185
2186 =head2 ReserveSlip
2187
2188 ReserveSlip(
2189     {
2190         branchcode     => $branchcode,
2191         borrowernumber => $borrowernumber,
2192         biblionumber   => $biblionumber,
2193         [ itemnumber   => $itemnumber, ]
2194         [ barcode      => $barcode, ]
2195     }
2196   )
2197
2198 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
2199
2200 The letter code will be HOLD_SLIP, and the following tables are
2201 available within the slip:
2202
2203     reserves
2204     branches
2205     borrowers
2206     biblio
2207     biblioitems
2208     items
2209
2210 =cut
2211
2212 sub ReserveSlip {
2213     my ($args) = @_;
2214     my $branchcode     = $args->{branchcode};
2215     my $reserve_id = $args->{reserve_id};
2216
2217     my $hold = Koha::Holds->find($reserve_id);
2218     return unless $hold;
2219
2220     my $patron = $hold->borrower;
2221     my $reserve = $hold->unblessed;
2222
2223     return  C4::Letters::GetPreparedLetter (
2224         module => 'circulation',
2225         letter_code => 'HOLD_SLIP',
2226         branchcode => $branchcode,
2227         lang => $patron->lang,
2228         tables => {
2229             'reserves'    => $reserve,
2230             'branches'    => $reserve->{branchcode},
2231             'borrowers'   => $reserve->{borrowernumber},
2232             'biblio'      => $reserve->{biblionumber},
2233             'biblioitems' => $reserve->{biblionumber},
2234             'items'       => $reserve->{itemnumber},
2235         },
2236     );
2237 }
2238
2239 =head2 GetReservesControlBranch
2240
2241   my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
2242
2243   Return the branchcode to be used to determine which reserves
2244   policy applies to a transaction.
2245
2246   C<$item> is a hashref for an item. Only 'homebranch' is used.
2247
2248   C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
2249
2250 =cut
2251
2252 sub GetReservesControlBranch {
2253     my ( $item, $borrower ) = @_;
2254
2255     my $reserves_control = C4::Context->preference('ReservesControlBranch');
2256
2257     my $branchcode =
2258         ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
2259       : ( $reserves_control eq 'PatronLibrary' )   ? $borrower->{'branchcode'}
2260       :                                              undef;
2261
2262     return $branchcode;
2263 }
2264
2265 =head2 CalculatePriority
2266
2267     my $p = CalculatePriority($biblionumber, $resdate);
2268
2269 Calculate priority for a new reserve on biblionumber, placing it at
2270 the end of the line of all holds whose start date falls before
2271 the current system time and that are neither on the hold shelf
2272 or in transit.
2273
2274 The reserve date parameter is optional; if it is supplied, the
2275 priority is based on the set of holds whose start date falls before
2276 the parameter value.
2277
2278 After calculation of this priority, it is recommended to call
2279 _ShiftPriority. Note that this is currently done in
2280 AddReserves.
2281
2282 =cut
2283
2284 sub CalculatePriority {
2285     my ( $biblionumber, $resdate ) = @_;
2286
2287     my $sql = q{
2288         SELECT COUNT(*) FROM reserves
2289         WHERE biblionumber = ?
2290         AND   priority > 0
2291         AND   (found IS NULL OR found = '')
2292     };
2293     #skip found==W or found==T or found==P (waiting, transit or processing holds)
2294     if( $resdate ) {
2295         $sql.= ' AND ( reservedate <= ? )';
2296     }
2297     else {
2298         $sql.= ' AND ( reservedate < NOW() )';
2299     }
2300     my $dbh = C4::Context->dbh();
2301     my @row = $dbh->selectrow_array(
2302         $sql,
2303         undef,
2304         $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2305     );
2306
2307     return @row ? $row[0]+1 : 1;
2308 }
2309
2310 =head2 IsItemOnHoldAndFound
2311
2312     my $bool = IsItemFoundHold( $itemnumber );
2313
2314     Returns true if the item is currently on hold
2315     and that hold has a non-null found status ( W, T, etc. )
2316
2317 =cut
2318
2319 sub IsItemOnHoldAndFound {
2320     my ($itemnumber) = @_;
2321
2322     my $rs = Koha::Database->new()->schema()->resultset('Reserve');
2323
2324     my $found = $rs->count(
2325         {
2326             itemnumber => $itemnumber,
2327             found      => { '!=' => undef }
2328         }
2329     );
2330
2331     return $found;
2332 }
2333
2334 =head2 GetMaxPatronHoldsForRecord
2335
2336 my $holds_per_record = ReservesControlBranch( $borrowernumber, $biblionumber );
2337
2338 For multiple holds on a given record for a given patron, the max
2339 number of record level holds that a patron can be placed is the highest
2340 value of the holds_per_record rule for each item if the record for that
2341 patron. This subroutine finds and returns the highest holds_per_record
2342 rule value for a given patron id and record id.
2343
2344 =cut
2345
2346 sub GetMaxPatronHoldsForRecord {
2347     my ( $borrowernumber, $biblionumber ) = @_;
2348
2349     my $patron = Koha::Patrons->find($borrowernumber);
2350     my @items = Koha::Items->search( { biblionumber => $biblionumber } )->as_list;
2351
2352     my $controlbranch = C4::Context->preference('ReservesControlBranch');
2353
2354     my $categorycode = $patron->categorycode;
2355     my $branchcode;
2356     $branchcode = $patron->branchcode if ( $controlbranch eq "PatronLibrary" );
2357
2358     my $max = 0;
2359     foreach my $item (@items) {
2360         my $itemtype = $item->effective_itemtype();
2361
2362         $branchcode = $item->homebranch if ( $controlbranch eq "ItemHomeLibrary" );
2363
2364         my $rule = Koha::CirculationRules->get_effective_rule({
2365             categorycode => $categorycode,
2366             itemtype     => $itemtype,
2367             branchcode   => $branchcode,
2368             rule_name    => 'holds_per_record'
2369         });
2370         my $holds_per_record = $rule ? $rule->rule_value : 0;
2371         $max = $holds_per_record if $holds_per_record > $max;
2372     }
2373
2374     return $max;
2375 }
2376
2377 =head1 AUTHOR
2378
2379 Koha Development Team <http://koha-community.org/>
2380
2381 =cut
2382
2383 1;