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