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