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