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