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