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