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