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