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