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