Bug 7534: Validate pickup location in CanBook/ItemBeReserved
[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, $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, $branchcode );
288         return $canReserve 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, $branchcode_to ) = @_;
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 ($branchcode_to) {
464         my $destination = Koha::Libraries->find({
465             branchcode => $branchcode_to,
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 );
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 ) = @_;
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' || $where eq 'down' ) {
1247
1248       my $priority = $hold->priority;
1249       $priority = $where eq 'up' ? $priority - 1 : $priority + 1;
1250       _FixPriority({ reserve_id => $reserve_id, rank => $priority })
1251
1252     } elsif ( $where eq 'top' ) {
1253
1254       _FixPriority({ reserve_id => $reserve_id, rank => '1' })
1255
1256     } elsif ( $where eq 'bottom' ) {
1257
1258       _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1259
1260     }
1261     # FIXME Should return the new priority
1262 }
1263
1264 =head2 ToggleLowestPriority
1265
1266   ToggleLowestPriority( $borrowernumber, $biblionumber );
1267
1268 This function sets the lowestPriority field to true if is false, and false if it is true.
1269
1270 =cut
1271
1272 sub ToggleLowestPriority {
1273     my ( $reserve_id ) = @_;
1274
1275     my $dbh = C4::Context->dbh;
1276
1277     my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1278     $sth->execute( $reserve_id );
1279
1280     _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1281 }
1282
1283 =head2 ToggleSuspend
1284
1285   ToggleSuspend( $reserve_id );
1286
1287 This function sets the suspend field to true if is false, and false if it is true.
1288 If the reserve is currently suspended with a suspend_until date, that date will
1289 be cleared when it is unsuspended.
1290
1291 =cut
1292
1293 sub ToggleSuspend {
1294     my ( $reserve_id, $suspend_until ) = @_;
1295
1296     $suspend_until = dt_from_string($suspend_until) if ($suspend_until);
1297
1298     my $hold = Koha::Holds->find( $reserve_id );
1299
1300     if ( $hold->is_suspended ) {
1301         $hold->resume()
1302     } else {
1303         $hold->suspend_hold( $suspend_until );
1304     }
1305 }
1306
1307 =head2 SuspendAll
1308
1309   SuspendAll(
1310       borrowernumber   => $borrowernumber,
1311       [ biblionumber   => $biblionumber, ]
1312       [ suspend_until  => $suspend_until, ]
1313       [ suspend        => $suspend ]
1314   );
1315
1316   This function accepts a set of hash keys as its parameters.
1317   It requires either borrowernumber or biblionumber, or both.
1318
1319   suspend_until is wholly optional.
1320
1321 =cut
1322
1323 sub SuspendAll {
1324     my %params = @_;
1325
1326     my $borrowernumber = $params{'borrowernumber'} || undef;
1327     my $biblionumber   = $params{'biblionumber'}   || undef;
1328     my $suspend_until  = $params{'suspend_until'}  || undef;
1329     my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1330
1331     $suspend_until = eval { dt_from_string($suspend_until) }
1332       if ( defined($suspend_until) );
1333
1334     return unless ( $borrowernumber || $biblionumber );
1335
1336     my $params;
1337     $params->{found}          = undef;
1338     $params->{borrowernumber} = $borrowernumber if $borrowernumber;
1339     $params->{biblionumber}   = $biblionumber if $biblionumber;
1340
1341     my @holds = Koha::Holds->search($params);
1342
1343     if ($suspend) {
1344         map { $_->suspend_hold($suspend_until) } @holds;
1345     }
1346     else {
1347         map { $_->resume() } @holds;
1348     }
1349 }
1350
1351
1352 =head2 _FixPriority
1353
1354   _FixPriority({
1355     reserve_id => $reserve_id,
1356     [rank => $rank,]
1357     [ignoreSetLowestRank => $ignoreSetLowestRank]
1358   });
1359
1360   or
1361
1362   _FixPriority({ biblionumber => $biblionumber});
1363
1364 This routine adjusts the priority of a hold request and holds
1365 on the same bib.
1366
1367 In the first form, where a reserve_id is passed, the priority of the
1368 hold is set to supplied rank, and other holds for that bib are adjusted
1369 accordingly.  If the rank is "del", the hold is cancelled.  If no rank
1370 is supplied, all of the holds on that bib have their priority adjusted
1371 as if the second form had been used.
1372
1373 In the second form, where a biblionumber is passed, the holds on that
1374 bib (that are not captured) are sorted in order of increasing priority,
1375 then have reserves.priority set so that the first non-captured hold
1376 has its priority set to 1, the second non-captured hold has its priority
1377 set to 2, and so forth.
1378
1379 In both cases, holds that have the lowestPriority flag on are have their
1380 priority adjusted to ensure that they remain at the end of the line.
1381
1382 Note that the ignoreSetLowestRank parameter is meant to be used only
1383 when _FixPriority calls itself.
1384
1385 =cut
1386
1387 sub _FixPriority {
1388     my ( $params ) = @_;
1389     my $reserve_id = $params->{reserve_id};
1390     my $rank = $params->{rank} // '';
1391     my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1392     my $biblionumber = $params->{biblionumber};
1393
1394     my $dbh = C4::Context->dbh;
1395
1396     my $hold;
1397     if ( $reserve_id ) {
1398         $hold = Koha::Holds->find( $reserve_id );
1399         return unless $hold;
1400     }
1401
1402     unless ( $biblionumber ) { # FIXME This is a very weird API
1403         $biblionumber = $hold->biblionumber;
1404     }
1405
1406     if ( $rank eq "del" ) { # FIXME will crash if called without $hold
1407         $hold->cancel;
1408     }
1409     elsif ( $rank eq "W" || $rank eq "0" ) {
1410
1411         # make sure priority for waiting or in-transit items is 0
1412         my $query = "
1413             UPDATE reserves
1414             SET    priority = 0
1415             WHERE reserve_id = ?
1416             AND found IN ('W', 'T')
1417         ";
1418         my $sth = $dbh->prepare($query);
1419         $sth->execute( $reserve_id );
1420     }
1421     my @priority;
1422
1423     # get whats left
1424     my $query = "
1425         SELECT reserve_id, borrowernumber, reservedate
1426         FROM   reserves
1427         WHERE  biblionumber   = ?
1428           AND  ((found <> 'W' AND found <> 'T') OR found IS NULL)
1429         ORDER BY priority ASC
1430     ";
1431     my $sth = $dbh->prepare($query);
1432     $sth->execute( $biblionumber );
1433     while ( my $line = $sth->fetchrow_hashref ) {
1434         push( @priority,     $line );
1435     }
1436
1437     # To find the matching index
1438     my $i;
1439     my $key = -1;    # to allow for 0 to be a valid result
1440     for ( $i = 0 ; $i < @priority ; $i++ ) {
1441         if ( $reserve_id == $priority[$i]->{'reserve_id'} ) {
1442             $key = $i;    # save the index
1443             last;
1444         }
1445     }
1446
1447     # if index exists in array then move it to new position
1448     if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1449         my $new_rank = $rank -
1450           1;    # $new_rank is what you want the new index to be in the array
1451         my $moving_item = splice( @priority, $key, 1 );
1452         splice( @priority, $new_rank, 0, $moving_item );
1453     }
1454
1455     # now fix the priority on those that are left....
1456     $query = "
1457         UPDATE reserves
1458         SET    priority = ?
1459         WHERE  reserve_id = ?
1460     ";
1461     $sth = $dbh->prepare($query);
1462     for ( my $j = 0 ; $j < @priority ; $j++ ) {
1463         $sth->execute(
1464             $j + 1,
1465             $priority[$j]->{'reserve_id'}
1466         );
1467     }
1468
1469     $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1470     $sth->execute();
1471
1472     unless ( $ignoreSetLowestRank ) {
1473       while ( my $res = $sth->fetchrow_hashref() ) {
1474         _FixPriority({
1475             reserve_id => $res->{'reserve_id'},
1476             rank => '999999',
1477             ignoreSetLowestRank => 1
1478         });
1479       }
1480     }
1481 }
1482
1483 =head2 _Findgroupreserve
1484
1485   @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead, $ignore_borrowers);
1486
1487 Looks for a holds-queue based item-specific match first, then for a holds-queue title-level match, returning the
1488 first match found.  If neither, then we look for non-holds-queue based holds.
1489 Lookahead is the number of days to look in advance.
1490
1491 C<&_Findgroupreserve> returns :
1492 C<@results> is an array of references-to-hash whose keys are mostly
1493 fields from the reserves table of the Koha database, plus
1494 C<biblioitemnumber>.
1495
1496 =cut
1497
1498 sub _Findgroupreserve {
1499     my ( $bibitem, $biblio, $itemnumber, $lookahead, $ignore_borrowers) = @_;
1500     my $dbh   = C4::Context->dbh;
1501
1502     # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1503     # check for exact targeted match
1504     my $item_level_target_query = qq{
1505         SELECT reserves.biblionumber        AS biblionumber,
1506                reserves.borrowernumber      AS borrowernumber,
1507                reserves.reservedate         AS reservedate,
1508                reserves.branchcode          AS branchcode,
1509                reserves.cancellationdate    AS cancellationdate,
1510                reserves.found               AS found,
1511                reserves.reservenotes        AS reservenotes,
1512                reserves.priority            AS priority,
1513                reserves.timestamp           AS timestamp,
1514                biblioitems.biblioitemnumber AS biblioitemnumber,
1515                reserves.itemnumber          AS itemnumber,
1516                reserves.reserve_id          AS reserve_id,
1517                reserves.itemtype            AS itemtype
1518         FROM reserves
1519         JOIN biblioitems USING (biblionumber)
1520         JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1521         WHERE found IS NULL
1522         AND priority > 0
1523         AND item_level_request = 1
1524         AND itemnumber = ?
1525         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1526         AND suspend = 0
1527         ORDER BY priority
1528     };
1529     my $sth = $dbh->prepare($item_level_target_query);
1530     $sth->execute($itemnumber, $lookahead||0);
1531     my @results;
1532     if ( my $data = $sth->fetchrow_hashref ) {
1533         push( @results, $data )
1534           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1535     }
1536     return @results if @results;
1537
1538     # check for title-level targeted match
1539     my $title_level_target_query = qq{
1540         SELECT reserves.biblionumber        AS biblionumber,
1541                reserves.borrowernumber      AS borrowernumber,
1542                reserves.reservedate         AS reservedate,
1543                reserves.branchcode          AS branchcode,
1544                reserves.cancellationdate    AS cancellationdate,
1545                reserves.found               AS found,
1546                reserves.reservenotes        AS reservenotes,
1547                reserves.priority            AS priority,
1548                reserves.timestamp           AS timestamp,
1549                biblioitems.biblioitemnumber AS biblioitemnumber,
1550                reserves.itemnumber          AS itemnumber,
1551                reserves.reserve_id          AS reserve_id,
1552                reserves.itemtype            AS itemtype
1553         FROM reserves
1554         JOIN biblioitems USING (biblionumber)
1555         JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1556         WHERE found IS NULL
1557         AND priority > 0
1558         AND item_level_request = 0
1559         AND hold_fill_targets.itemnumber = ?
1560         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1561         AND suspend = 0
1562         ORDER BY priority
1563     };
1564     $sth = $dbh->prepare($title_level_target_query);
1565     $sth->execute($itemnumber, $lookahead||0);
1566     @results = ();
1567     if ( my $data = $sth->fetchrow_hashref ) {
1568         push( @results, $data )
1569           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1570     }
1571     return @results if @results;
1572
1573     my $query = qq{
1574         SELECT reserves.biblionumber               AS biblionumber,
1575                reserves.borrowernumber             AS borrowernumber,
1576                reserves.reservedate                AS reservedate,
1577                reserves.waitingdate                AS waitingdate,
1578                reserves.branchcode                 AS branchcode,
1579                reserves.cancellationdate           AS cancellationdate,
1580                reserves.found                      AS found,
1581                reserves.reservenotes               AS reservenotes,
1582                reserves.priority                   AS priority,
1583                reserves.timestamp                  AS timestamp,
1584                reserves.itemnumber                 AS itemnumber,
1585                reserves.reserve_id                 AS reserve_id,
1586                reserves.itemtype                   AS itemtype
1587         FROM reserves
1588         WHERE reserves.biblionumber = ?
1589           AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1590           AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1591           AND suspend = 0
1592           ORDER BY priority
1593     };
1594     $sth = $dbh->prepare($query);
1595     $sth->execute( $biblio, $itemnumber, $lookahead||0);
1596     @results = ();
1597     while ( my $data = $sth->fetchrow_hashref ) {
1598         push( @results, $data )
1599           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1600     }
1601     return @results;
1602 }
1603
1604 =head2 _koha_notify_reserve
1605
1606   _koha_notify_reserve( $hold->reserve_id );
1607
1608 Sends a notification to the patron that their hold has been filled (through
1609 ModReserveAffect, _not_ ModReserveFill)
1610
1611 The letter code for this notice may be found using the following query:
1612
1613     select distinct letter_code
1614     from message_transports
1615     inner join message_attributes using (message_attribute_id)
1616     where message_name = 'Hold_Filled'
1617
1618 This will probably sipmly be 'HOLD', but because it is defined in the database,
1619 it is subject to addition or change.
1620
1621 The following tables are availalbe witin the notice:
1622
1623     branches
1624     borrowers
1625     biblio
1626     biblioitems
1627     reserves
1628     items
1629
1630 =cut
1631
1632 sub _koha_notify_reserve {
1633     my $reserve_id = shift;
1634     my $hold = Koha::Holds->find($reserve_id);
1635     my $borrowernumber = $hold->borrowernumber;
1636
1637     my $patron = Koha::Patrons->find( $borrowernumber );
1638
1639     # Try to get the borrower's email address
1640     my $to_address = $patron->notice_email_address;
1641
1642     my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
1643             borrowernumber => $borrowernumber,
1644             message_name => 'Hold_Filled'
1645     } );
1646
1647     my $library = Koha::Libraries->find( $hold->branchcode )->unblessed;
1648
1649     my $admin_email_address = $library->{branchemail} || C4::Context->preference('KohaAdminEmailAddress');
1650
1651     my %letter_params = (
1652         module => 'reserves',
1653         branchcode => $hold->branchcode,
1654         lang => $patron->lang,
1655         tables => {
1656             'branches'       => $library,
1657             'borrowers'      => $patron->unblessed,
1658             'biblio'         => $hold->biblionumber,
1659             'biblioitems'    => $hold->biblionumber,
1660             'reserves'       => $hold->unblessed,
1661             'items'          => $hold->itemnumber,
1662         },
1663     );
1664
1665     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.
1666     my $send_notification = sub {
1667         my ( $mtt, $letter_code ) = (@_);
1668         return unless defined $letter_code;
1669         $letter_params{letter_code} = $letter_code;
1670         $letter_params{message_transport_type} = $mtt;
1671         my $letter =  C4::Letters::GetPreparedLetter ( %letter_params );
1672         unless ($letter) {
1673             warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
1674             return;
1675         }
1676
1677         C4::Letters::EnqueueLetter( {
1678             letter => $letter,
1679             borrowernumber => $borrowernumber,
1680             from_address => $admin_email_address,
1681             message_transport_type => $mtt,
1682         } );
1683     };
1684
1685     while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
1686         next if (
1687                ( $mtt eq 'email' and not $to_address ) # No email address
1688             or ( $mtt eq 'sms'   and not $patron->smsalertnumber ) # No SMS number
1689             or ( $mtt eq 'phone' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl
1690         );
1691
1692         &$send_notification($mtt, $letter_code);
1693         $notification_sent++;
1694     }
1695     #Making sure that a print notification is sent if no other transport types can be utilized.
1696     if (! $notification_sent) {
1697         &$send_notification('print', 'HOLD');
1698     }
1699
1700 }
1701
1702 =head2 _ShiftPriorityByDateAndPriority
1703
1704   $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1705
1706 This increments the priority of all reserves after the one
1707 with either the lowest date after C<$reservedate>
1708 or the lowest priority after C<$priority>.
1709
1710 It effectively makes room for a new reserve to be inserted with a certain
1711 priority, which is returned.
1712
1713 This is most useful when the reservedate can be set by the user.  It allows
1714 the new reserve to be placed before other reserves that have a later
1715 reservedate.  Since priority also is set by the form in reserves/request.pl
1716 the sub accounts for that too.
1717
1718 =cut
1719
1720 sub _ShiftPriorityByDateAndPriority {
1721     my ( $biblio, $resdate, $new_priority ) = @_;
1722
1723     my $dbh = C4::Context->dbh;
1724     my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1725     my $sth = $dbh->prepare( $query );
1726     $sth->execute( $biblio, $resdate, $new_priority );
1727     my $min_priority = $sth->fetchrow;
1728     # if no such matches are found, $new_priority remains as original value
1729     $new_priority = $min_priority if ( $min_priority );
1730
1731     # Shift the priority up by one; works in conjunction with the next SQL statement
1732     $query = "UPDATE reserves
1733               SET priority = priority+1
1734               WHERE biblionumber = ?
1735               AND borrowernumber = ?
1736               AND reservedate = ?
1737               AND found IS NULL";
1738     my $sth_update = $dbh->prepare( $query );
1739
1740     # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1741     $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1742     $sth = $dbh->prepare( $query );
1743     $sth->execute( $new_priority, $biblio );
1744     while ( my $row = $sth->fetchrow_hashref ) {
1745         $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
1746     }
1747
1748     return $new_priority;  # so the caller knows what priority they wind up receiving
1749 }
1750
1751 =head2 MoveReserve
1752
1753   MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
1754
1755 Use when checking out an item to handle reserves
1756 If $cancelreserve boolean is set to true, it will remove existing reserve
1757
1758 =cut
1759
1760 sub MoveReserve {
1761     my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
1762
1763     my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
1764     my ( $restype, $res, $all_reserves ) = CheckReserves( $itemnumber, undef, $lookahead );
1765     return unless $res;
1766
1767     my $biblionumber     =  $res->{biblionumber};
1768
1769     if ($res->{borrowernumber} == $borrowernumber) {
1770         ModReserveFill($res);
1771     }
1772     else {
1773         # warn "Reserved";
1774         # The item is reserved by someone else.
1775         # Find this item in the reserves
1776
1777         my $borr_res;
1778         foreach (@$all_reserves) {
1779             $_->{'borrowernumber'} == $borrowernumber or next;
1780             $_->{'biblionumber'}   == $biblionumber   or next;
1781
1782             $borr_res = $_;
1783             last;
1784         }
1785
1786         if ( $borr_res ) {
1787             # The item is reserved by the current patron
1788             ModReserveFill($borr_res);
1789         }
1790
1791         if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
1792             RevertWaitingStatus({ itemnumber => $itemnumber });
1793         }
1794         elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
1795             my $hold = Koha::Holds->find( $res->{reserve_id} );
1796             $hold->cancel;
1797         }
1798     }
1799 }
1800
1801 =head2 MergeHolds
1802
1803   MergeHolds($dbh,$to_biblio, $from_biblio);
1804
1805 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
1806
1807 =cut
1808
1809 sub MergeHolds {
1810     my ( $dbh, $to_biblio, $from_biblio ) = @_;
1811     my $sth = $dbh->prepare(
1812         "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
1813     );
1814     $sth->execute($from_biblio);
1815     if ( my $data = $sth->fetchrow_hashref() ) {
1816
1817         # holds exist on old record, if not we don't need to do anything
1818         $sth = $dbh->prepare(
1819             "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
1820         $sth->execute( $to_biblio, $from_biblio );
1821
1822         # Reorder by date
1823         # don't reorder those already waiting
1824
1825         $sth = $dbh->prepare(
1826 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
1827         );
1828         my $upd_sth = $dbh->prepare(
1829 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
1830         AND reservedate = ? AND (itemnumber = ? or itemnumber is NULL) "
1831         );
1832         $sth->execute( $to_biblio, 'W', 'T' );
1833         my $priority = 1;
1834         while ( my $reserve = $sth->fetchrow_hashref() ) {
1835             $upd_sth->execute(
1836                 $priority,                    $to_biblio,
1837                 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
1838                 $reserve->{'itemnumber'}
1839             );
1840             $priority++;
1841         }
1842     }
1843 }
1844
1845 =head2 RevertWaitingStatus
1846
1847   RevertWaitingStatus({ itemnumber => $itemnumber });
1848
1849   Reverts a 'waiting' hold back to a regular hold with a priority of 1.
1850
1851   Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
1852           item level hold, even if it was only a bibliolevel hold to
1853           begin with. This is because we can no longer know if a hold
1854           was item-level or bib-level after a hold has been set to
1855           waiting status.
1856
1857 =cut
1858
1859 sub RevertWaitingStatus {
1860     my ( $params ) = @_;
1861     my $itemnumber = $params->{'itemnumber'};
1862
1863     return unless ( $itemnumber );
1864
1865     my $dbh = C4::Context->dbh;
1866
1867     ## Get the waiting reserve we want to revert
1868     my $query = "
1869         SELECT * FROM reserves
1870         WHERE itemnumber = ?
1871         AND found IS NOT NULL
1872     ";
1873     my $sth = $dbh->prepare( $query );
1874     $sth->execute( $itemnumber );
1875     my $reserve = $sth->fetchrow_hashref();
1876
1877     ## Increment the priority of all other non-waiting
1878     ## reserves for this bib record
1879     $query = "
1880         UPDATE reserves
1881         SET
1882           priority = priority + 1
1883         WHERE
1884           biblionumber =  ?
1885         AND
1886           priority > 0
1887     ";
1888     $sth = $dbh->prepare( $query );
1889     $sth->execute( $reserve->{'biblionumber'} );
1890
1891     ## Fix up the currently waiting reserve
1892     $query = "
1893     UPDATE reserves
1894     SET
1895       priority = 1,
1896       found = NULL,
1897       waitingdate = NULL
1898     WHERE
1899       reserve_id = ?
1900     ";
1901     $sth = $dbh->prepare( $query );
1902     $sth->execute( $reserve->{'reserve_id'} );
1903     _FixPriority( { biblionumber => $reserve->{biblionumber} } );
1904 }
1905
1906 =head2 ReserveSlip
1907
1908 ReserveSlip(
1909     {
1910         branchcode     => $branchcode,
1911         borrowernumber => $borrowernumber,
1912         biblionumber   => $biblionumber,
1913         [ itemnumber   => $itemnumber, ]
1914         [ barcode      => $barcode, ]
1915     }
1916   )
1917
1918 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
1919
1920 The letter code will be HOLD_SLIP, and the following tables are
1921 available within the slip:
1922
1923     reserves
1924     branches
1925     borrowers
1926     biblio
1927     biblioitems
1928     items
1929
1930 =cut
1931
1932 sub ReserveSlip {
1933     my ($args) = @_;
1934     my $branchcode     = $args->{branchcode};
1935     my $borrowernumber = $args->{borrowernumber};
1936     my $biblionumber   = $args->{biblionumber};
1937     my $itemnumber     = $args->{itemnumber};
1938     my $barcode        = $args->{barcode};
1939
1940
1941     my $patron = Koha::Patrons->find($borrowernumber);
1942
1943     my $hold;
1944     if ($itemnumber || $barcode ) {
1945         $itemnumber ||= Koha::Items->find( { barcode => $barcode } )->itemnumber;
1946
1947         $hold = Koha::Holds->search(
1948             {
1949                 biblionumber   => $biblionumber,
1950                 borrowernumber => $borrowernumber,
1951                 itemnumber     => $itemnumber
1952             }
1953         )->next;
1954     }
1955     else {
1956         $hold = Koha::Holds->search(
1957             {
1958                 biblionumber   => $biblionumber,
1959                 borrowernumber => $borrowernumber
1960             }
1961         )->next;
1962     }
1963
1964     return unless $hold;
1965     my $reserve = $hold->unblessed;
1966
1967     return  C4::Letters::GetPreparedLetter (
1968         module => 'circulation',
1969         letter_code => 'HOLD_SLIP',
1970         branchcode => $branchcode,
1971         lang => $patron->lang,
1972         tables => {
1973             'reserves'    => $reserve,
1974             'branches'    => $reserve->{branchcode},
1975             'borrowers'   => $reserve->{borrowernumber},
1976             'biblio'      => $reserve->{biblionumber},
1977             'biblioitems' => $reserve->{biblionumber},
1978             'items'       => $reserve->{itemnumber},
1979         },
1980     );
1981 }
1982
1983 =head2 GetReservesControlBranch
1984
1985   my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
1986
1987   Return the branchcode to be used to determine which reserves
1988   policy applies to a transaction.
1989
1990   C<$item> is a hashref for an item. Only 'homebranch' is used.
1991
1992   C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
1993
1994 =cut
1995
1996 sub GetReservesControlBranch {
1997     my ( $item, $borrower ) = @_;
1998
1999     my $reserves_control = C4::Context->preference('ReservesControlBranch');
2000
2001     my $branchcode =
2002         ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
2003       : ( $reserves_control eq 'PatronLibrary' )   ? $borrower->{'branchcode'}
2004       :                                              undef;
2005
2006     return $branchcode;
2007 }
2008
2009 =head2 CalculatePriority
2010
2011     my $p = CalculatePriority($biblionumber, $resdate);
2012
2013 Calculate priority for a new reserve on biblionumber, placing it at
2014 the end of the line of all holds whose start date falls before
2015 the current system time and that are neither on the hold shelf
2016 or in transit.
2017
2018 The reserve date parameter is optional; if it is supplied, the
2019 priority is based on the set of holds whose start date falls before
2020 the parameter value.
2021
2022 After calculation of this priority, it is recommended to call
2023 _ShiftPriorityByDateAndPriority. Note that this is currently done in
2024 AddReserves.
2025
2026 =cut
2027
2028 sub CalculatePriority {
2029     my ( $biblionumber, $resdate ) = @_;
2030
2031     my $sql = q{
2032         SELECT COUNT(*) FROM reserves
2033         WHERE biblionumber = ?
2034         AND   priority > 0
2035         AND   (found IS NULL OR found = '')
2036     };
2037     #skip found==W or found==T (waiting or transit holds)
2038     if( $resdate ) {
2039         $sql.= ' AND ( reservedate <= ? )';
2040     }
2041     else {
2042         $sql.= ' AND ( reservedate < NOW() )';
2043     }
2044     my $dbh = C4::Context->dbh();
2045     my @row = $dbh->selectrow_array(
2046         $sql,
2047         undef,
2048         $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2049     );
2050
2051     return @row ? $row[0]+1 : 1;
2052 }
2053
2054 =head2 IsItemOnHoldAndFound
2055
2056     my $bool = IsItemFoundHold( $itemnumber );
2057
2058     Returns true if the item is currently on hold
2059     and that hold has a non-null found status ( W, T, etc. )
2060
2061 =cut
2062
2063 sub IsItemOnHoldAndFound {
2064     my ($itemnumber) = @_;
2065
2066     my $rs = Koha::Database->new()->schema()->resultset('Reserve');
2067
2068     my $found = $rs->count(
2069         {
2070             itemnumber => $itemnumber,
2071             found      => { '!=' => undef }
2072         }
2073     );
2074
2075     return $found;
2076 }
2077
2078 =head2 GetMaxPatronHoldsForRecord
2079
2080 my $holds_per_record = ReservesControlBranch( $borrowernumber, $biblionumber );
2081
2082 For multiple holds on a given record for a given patron, the max
2083 number of record level holds that a patron can be placed is the highest
2084 value of the holds_per_record rule for each item if the record for that
2085 patron. This subroutine finds and returns the highest holds_per_record
2086 rule value for a given patron id and record id.
2087
2088 =cut
2089
2090 sub GetMaxPatronHoldsForRecord {
2091     my ( $borrowernumber, $biblionumber ) = @_;
2092
2093     my $patron = Koha::Patrons->find($borrowernumber);
2094     my @items = Koha::Items->search( { biblionumber => $biblionumber } );
2095
2096     my $controlbranch = C4::Context->preference('ReservesControlBranch');
2097
2098     my $categorycode = $patron->categorycode;
2099     my $branchcode;
2100     $branchcode = $patron->branchcode if ( $controlbranch eq "PatronLibrary" );
2101
2102     my $max = 0;
2103     foreach my $item (@items) {
2104         my $itemtype = $item->effective_itemtype();
2105
2106         $branchcode = $item->homebranch if ( $controlbranch eq "ItemHomeLibrary" );
2107
2108         my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2109         my $holds_per_record = $rule ? $rule->{holds_per_record} : 0;
2110         $max = $holds_per_record if $holds_per_record > $max;
2111     }
2112
2113     return $max;
2114 }
2115
2116 =head2 GetHoldRule
2117
2118 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2119
2120 Returns the matching hold related issuingrule fields for a given
2121 patron category, itemtype, and library.
2122
2123 =cut
2124
2125 sub GetHoldRule {
2126     my ( $categorycode, $itemtype, $branchcode ) = @_;
2127
2128     my $dbh = C4::Context->dbh;
2129
2130     my $sth = $dbh->prepare(
2131         q{
2132          SELECT categorycode, itemtype, branchcode, reservesallowed, holds_per_record
2133            FROM issuingrules
2134           WHERE (categorycode in (?,'*') )
2135             AND (itemtype IN (?,'*'))
2136             AND (branchcode IN (?,'*'))
2137        ORDER BY categorycode DESC,
2138                 itemtype     DESC,
2139                 branchcode   DESC
2140         }
2141     );
2142
2143     $sth->execute( $categorycode, $itemtype, $branchcode );
2144
2145     return $sth->fetchrow_hashref();
2146 }
2147
2148 =head1 AUTHOR
2149
2150 Koha Development Team <http://koha-community.org/>
2151
2152 =cut
2153
2154 1;