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