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