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