Bug 12876 - Reserve in waiting/transfer status may be cancelled by user
[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     my $reserve = GetReserve($reserve_id);
591
592     return 0 unless $reserve->{borrowernumber} == $borrowernumber;
593     return 0 if ( $reserve->{found} eq 'W' ) or ( $reserve->{found} eq 'T' );
594
595     return 1;
596
597 }
598
599 #--------------------------------------------------------------------------------
600 =head2 GetReserveCount
601
602   $number = &GetReserveCount($borrowernumber);
603
604 this function returns the number of reservation for a borrower given on input arg.
605
606 =cut
607
608 sub GetReserveCount {
609     my ($borrowernumber) = @_;
610
611     my $dbh = C4::Context->dbh;
612
613     my $query = "
614         SELECT COUNT(*) AS counter
615         FROM reserves
616         WHERE borrowernumber = ?
617     ";
618     my $sth = $dbh->prepare($query);
619     $sth->execute($borrowernumber);
620     my $row = $sth->fetchrow_hashref;
621     return $row->{counter};
622 }
623
624 =head2 GetOtherReserves
625
626   ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
627
628 Check queued list of this document and check if this document must be  transfered
629
630 =cut
631
632 sub GetOtherReserves {
633     my ($itemnumber) = @_;
634     my $messages;
635     my $nextreservinfo;
636     my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber);
637     if ($checkreserves) {
638         my $iteminfo = GetItem($itemnumber);
639         if ( $iteminfo->{'holdingbranch'} ne $checkreserves->{'branchcode'} ) {
640             $messages->{'transfert'} = $checkreserves->{'branchcode'};
641             #minus priorities of others reservs
642             ModReserveMinusPriority(
643                 $itemnumber,
644                 $checkreserves->{'reserve_id'},
645             );
646
647             #launch the subroutine dotransfer
648             C4::Items::ModItemTransfer(
649                 $itemnumber,
650                 $iteminfo->{'holdingbranch'},
651                 $checkreserves->{'branchcode'}
652               ),
653               ;
654         }
655
656      #step 2b : case of a reservation on the same branch, set the waiting status
657         else {
658             $messages->{'waiting'} = 1;
659             ModReserveMinusPriority(
660                 $itemnumber,
661                 $checkreserves->{'reserve_id'},
662             );
663             ModReserveStatus($itemnumber,'W');
664         }
665
666         $nextreservinfo = $checkreserves->{'borrowernumber'};
667     }
668
669     return ( $messages, $nextreservinfo );
670 }
671
672 =head2 GetReserveFee
673
674   $fee = GetReserveFee($borrowernumber,$biblionumber,$constraint,$biblionumber);
675
676 Calculate the fee for a reserve
677
678 =cut
679
680 sub GetReserveFee {
681     my ($borrowernumber, $biblionumber, $constraint, $bibitems ) = @_;
682
683     #check for issues;
684     my $dbh   = C4::Context->dbh;
685     my $const = lc substr( $constraint, 0, 1 );
686     my $query = qq/
687       SELECT * FROM borrowers
688     LEFT JOIN categories ON borrowers.categorycode = categories.categorycode
689     WHERE borrowernumber = ?
690     /;
691     my $sth = $dbh->prepare($query);
692     $sth->execute($borrowernumber);
693     my $data = $sth->fetchrow_hashref;
694     my $fee      = $data->{'reservefee'};
695     my $cntitems = @- > $bibitems;
696
697     if ( $fee > 0 ) {
698
699         # check for items on issue
700         # first find biblioitem records
701         my @biblioitems;
702         my $sth1 = $dbh->prepare(
703             "SELECT * FROM biblio LEFT JOIN biblioitems on biblio.biblionumber = biblioitems.biblionumber
704                    WHERE (biblio.biblionumber = ?)"
705         );
706         $sth1->execute($biblionumber);
707         while ( my $data1 = $sth1->fetchrow_hashref ) {
708             if ( $const eq "a" ) {
709                 push @biblioitems, $data1;
710             }
711             else {
712                 my $found = 0;
713                 my $x     = 0;
714                 while ( $x < $cntitems ) {
715                     if ( @$bibitems->{'biblioitemnumber'} ==
716                         $data->{'biblioitemnumber'} )
717                     {
718                         $found = 1;
719                     }
720                     $x++;
721                 }
722                 if ( $const eq 'o' ) {
723                     if ( $found == 1 ) {
724                         push @biblioitems, $data1;
725                     }
726                 }
727                 else {
728                     if ( $found == 0 ) {
729                         push @biblioitems, $data1;
730                     }
731                 }
732             }
733         }
734         my $cntitemsfound = @biblioitems;
735         my $issues        = 0;
736         my $x             = 0;
737         my $allissued     = 1;
738         while ( $x < $cntitemsfound ) {
739             my $bitdata = $biblioitems[$x];
740             my $sth2    = $dbh->prepare(
741                 "SELECT * FROM items
742                      WHERE biblioitemnumber = ?"
743             );
744             $sth2->execute( $bitdata->{'biblioitemnumber'} );
745             while ( my $itdata = $sth2->fetchrow_hashref ) {
746                 my $sth3 = $dbh->prepare(
747                     "SELECT * FROM issues
748                        WHERE itemnumber = ?"
749                 );
750                 $sth3->execute( $itdata->{'itemnumber'} );
751                 if ( my $isdata = $sth3->fetchrow_hashref ) {
752                 }
753                 else {
754                     $allissued = 0;
755                 }
756             }
757             $x++;
758         }
759         if ( $allissued == 0 ) {
760             my $rsth =
761               $dbh->prepare("SELECT * FROM reserves WHERE biblionumber = ?");
762             $rsth->execute($biblionumber);
763             if ( my $rdata = $rsth->fetchrow_hashref ) {
764             }
765             else {
766                 $fee = 0;
767             }
768         }
769     }
770     return $fee;
771 }
772
773 =head2 GetReservesToBranch
774
775   @transreserv = GetReservesToBranch( $frombranch );
776
777 Get reserve list for a given branch
778
779 =cut
780
781 sub GetReservesToBranch {
782     my ( $frombranch ) = @_;
783     my $dbh = C4::Context->dbh;
784     my $sth = $dbh->prepare(
785         "SELECT reserve_id,borrowernumber,reservedate,itemnumber,timestamp
786          FROM reserves 
787          WHERE priority='0' 
788            AND branchcode=?"
789     );
790     $sth->execute( $frombranch );
791     my @transreserv;
792     my $i = 0;
793     while ( my $data = $sth->fetchrow_hashref ) {
794         $transreserv[$i] = $data;
795         $i++;
796     }
797     return (@transreserv);
798 }
799
800 =head2 GetReservesForBranch
801
802   @transreserv = GetReservesForBranch($frombranch);
803
804 =cut
805
806 sub GetReservesForBranch {
807     my ($frombranch) = @_;
808     my $dbh = C4::Context->dbh;
809
810     my $query = "
811         SELECT reserve_id,borrowernumber,reservedate,itemnumber,waitingdate
812         FROM   reserves 
813         WHERE   priority='0'
814         AND found='W'
815     ";
816     $query .= " AND branchcode=? " if ( $frombranch );
817     $query .= "ORDER BY waitingdate" ;
818
819     my $sth = $dbh->prepare($query);
820     if ($frombranch){
821      $sth->execute($frombranch);
822     } else {
823         $sth->execute();
824     }
825
826     my @transreserv;
827     my $i = 0;
828     while ( my $data = $sth->fetchrow_hashref ) {
829         $transreserv[$i] = $data;
830         $i++;
831     }
832     return (@transreserv);
833 }
834
835 =head2 GetReserveStatus
836
837   $reservestatus = GetReserveStatus($itemnumber, $biblionumber);
838
839 Take an itemnumber or a biblionumber and return the status of the reserve places on it.
840 If several reserves exist, the reserve with the lower priority is given.
841
842 =cut
843
844 ## FIXME: I don't think this does what it thinks it does.
845 ## It only ever checks the first reserve result, even though
846 ## multiple reserves for that bib can have the itemnumber set
847 ## the sub is only used once in the codebase.
848 sub GetReserveStatus {
849     my ($itemnumber, $biblionumber) = @_;
850
851     my $dbh = C4::Context->dbh;
852
853     my ($sth, $found, $priority);
854     if ( $itemnumber ) {
855         $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
856         $sth->execute($itemnumber);
857         ($found, $priority) = $sth->fetchrow_array;
858     }
859
860     if ( $biblionumber and not defined $found and not defined $priority ) {
861         $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE biblionumber = ? order by priority LIMIT 1");
862         $sth->execute($biblionumber);
863         ($found, $priority) = $sth->fetchrow_array;
864     }
865
866     if(defined $found) {
867         return 'Waiting'  if $found eq 'W' and $priority == 0;
868         return 'Finished' if $found eq 'F';
869     }
870
871     return 'Reserved' if $priority > 0;
872
873     return ''; # empty string here will remove need for checking undef, or less log lines
874 }
875
876 =head2 CheckReserves
877
878   ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber);
879   ($status, $reserve, $all_reserves) = &CheckReserves(undef, $barcode);
880   ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber,undef,$lookahead);
881
882 Find a book in the reserves.
883
884 C<$itemnumber> is the book's item number.
885 C<$lookahead> is the number of days to look in advance for future reserves.
886
887 As I understand it, C<&CheckReserves> looks for the given item in the
888 reserves. If it is found, that's a match, and C<$status> is set to
889 C<Waiting>.
890
891 Otherwise, it finds the most important item in the reserves with the
892 same biblio number as this book (I'm not clear on this) and returns it
893 with C<$status> set to C<Reserved>.
894
895 C<&CheckReserves> returns a two-element list:
896
897 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
898
899 C<$reserve> is the reserve item that matched. It is a
900 reference-to-hash whose keys are mostly the fields of the reserves
901 table in the Koha database.
902
903 =cut
904
905 sub CheckReserves {
906     my ( $item, $barcode, $lookahead_days) = @_;
907     my $dbh = C4::Context->dbh;
908     my $sth;
909     my $select;
910     if (C4::Context->preference('item-level_itypes')){
911         $select = "
912            SELECT items.biblionumber,
913            items.biblioitemnumber,
914            itemtypes.notforloan,
915            items.notforloan AS itemnotforloan,
916            items.itemnumber,
917            items.damaged
918            FROM   items
919            LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
920            LEFT JOIN itemtypes   ON items.itype   = itemtypes.itemtype
921         ";
922     }
923     else {
924         $select = "
925            SELECT items.biblionumber,
926            items.biblioitemnumber,
927            itemtypes.notforloan,
928            items.notforloan AS itemnotforloan,
929            items.itemnumber,
930            items.damaged
931            FROM   items
932            LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
933            LEFT JOIN itemtypes   ON biblioitems.itemtype   = itemtypes.itemtype
934         ";
935     }
936    
937     if ($item) {
938         $sth = $dbh->prepare("$select WHERE itemnumber = ?");
939         $sth->execute($item);
940     }
941     else {
942         $sth = $dbh->prepare("$select WHERE barcode = ?");
943         $sth->execute($barcode);
944     }
945     # note: we get the itemnumber because we might have started w/ just the barcode.  Now we know for sure we have it.
946     my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber, $damaged ) = $sth->fetchrow_array;
947
948     return if ( $damaged && !C4::Context->preference('AllowHoldsOnDamagedItems') );
949
950     return unless $itemnumber; # bail if we got nothing.
951
952     # if item is not for loan it cannot be reserved either.....
953     #    execpt where items.notforloan < 0 :  This indicates the item is holdable. 
954     return if  ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
955
956     # Find this item in the reserves
957     my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber, $lookahead_days);
958
959     # $priority and $highest are used to find the most important item
960     # in the list returned by &_Findgroupreserve. (The lower $priority,
961     # the more important the item.)
962     # $highest is the most important item we've seen so far.
963     my $highest;
964     if (scalar @reserves) {
965         my $priority = 10000000;
966         foreach my $res (@reserves) {
967             if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
968                 return ( "Waiting", $res, \@reserves ); # Found it
969             } else {
970                 # See if this item is more important than what we've got so far
971                 if ( $res->{'priority'} && $res->{'priority'} < $priority ) {
972                     my $borrowerinfo=C4::Members::GetMember(borrowernumber => $res->{'borrowernumber'});
973                     my $iteminfo=C4::Items::GetItem($itemnumber);
974                     my $branch = GetReservesControlBranch( $iteminfo, $borrowerinfo );
975                     my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$iteminfo->{'itype'});
976                     next if ($branchitemrule->{'holdallowed'} == 0);
977                     next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $borrowerinfo->{'branchcode'}));
978                     $priority = $res->{'priority'};
979                     $highest  = $res;
980                 }
981             }
982         }
983     }
984
985     # If we get this far, then no exact match was found.
986     # We return the most important (i.e. next) reservation.
987     if ($highest) {
988         $highest->{'itemnumber'} = $item;
989         return ( "Reserved", $highest, \@reserves );
990     }
991
992     return ( '' );
993 }
994
995 =head2 CancelExpiredReserves
996
997   CancelExpiredReserves();
998
999 Cancels all reserves with an expiration date from before today.
1000
1001 =cut
1002
1003 sub CancelExpiredReserves {
1004
1005     # Cancel reserves that have passed their expiration date.
1006     my $dbh = C4::Context->dbh;
1007     my $sth = $dbh->prepare( "
1008         SELECT * FROM reserves WHERE DATE(expirationdate) < DATE( CURDATE() ) 
1009         AND expirationdate IS NOT NULL
1010         AND found IS NULL
1011     " );
1012     $sth->execute();
1013
1014     while ( my $res = $sth->fetchrow_hashref() ) {
1015         CancelReserve({ reserve_id => $res->{'reserve_id'} });
1016     }
1017   
1018     # Cancel reserves that have been waiting too long
1019     if ( C4::Context->preference("ExpireReservesMaxPickUpDelay") ) {
1020         my $max_pickup_delay = C4::Context->preference("ReservesMaxPickUpDelay");
1021         my $charge = C4::Context->preference("ExpireReservesMaxPickUpDelayCharge");
1022         my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
1023
1024         my $today = dt_from_string();
1025
1026         my $query = "SELECT * FROM reserves WHERE TO_DAYS( NOW() ) - TO_DAYS( waitingdate ) > ? AND found = 'W' AND priority = 0";
1027         $sth = $dbh->prepare( $query );
1028         $sth->execute( $max_pickup_delay );
1029
1030         while ( my $res = $sth->fetchrow_hashref ) {
1031             my $do_cancel = 1;
1032             unless ( $cancel_on_holidays ) {
1033                 my $calendar = Koha::Calendar->new( branchcode => $res->{'branchcode'} );
1034                 my $is_holiday = $calendar->is_holiday( $today );
1035
1036                 if ( $is_holiday ) {
1037                     $do_cancel = 0;
1038                 }
1039             }
1040
1041             if ( $do_cancel ) {
1042                 if ( $charge ) {
1043                     manualinvoice($res->{'borrowernumber'}, $res->{'itemnumber'}, 'Hold waiting too long', 'F', $charge);
1044                 }
1045
1046                 CancelReserve({ reserve_id => $res->{'reserve_id'} });
1047             }
1048         }
1049     }
1050
1051 }
1052
1053 =head2 AutoUnsuspendReserves
1054
1055   AutoUnsuspendReserves();
1056
1057 Unsuspends all suspended reserves with a suspend_until date from before today.
1058
1059 =cut
1060
1061 sub AutoUnsuspendReserves {
1062
1063     my $dbh = C4::Context->dbh;
1064
1065     my $query = "UPDATE reserves SET suspend = 0, suspend_until = NULL WHERE DATE( suspend_until ) < DATE( CURDATE() )";
1066     my $sth = $dbh->prepare( $query );
1067     $sth->execute();
1068
1069 }
1070
1071 =head2 CancelReserve
1072
1073   CancelReserve({ reserve_id => $reserve_id, [ biblionumber => $biblionumber, borrowernumber => $borrrowernumber, itemnumber => $itemnumber ] });
1074
1075 Cancels a reserve.
1076
1077 =cut
1078
1079 sub CancelReserve {
1080     my ( $params ) = @_;
1081
1082     my $reserve_id = $params->{'reserve_id'};
1083     $reserve_id = GetReserveId( $params ) unless ( $reserve_id );
1084
1085     return unless ( $reserve_id );
1086
1087     my $dbh = C4::Context->dbh;
1088
1089     my $reserve = GetReserve( $reserve_id );
1090     if ($reserve) {
1091         my $query = "
1092             UPDATE reserves
1093             SET    cancellationdate = now(),
1094                    found            = Null,
1095                    priority         = 0
1096             WHERE  reserve_id = ?
1097         ";
1098         my $sth = $dbh->prepare($query);
1099         $sth->execute( $reserve_id );
1100
1101         $query = "
1102             INSERT INTO old_reserves
1103             SELECT * FROM reserves
1104             WHERE  reserve_id = ?
1105         ";
1106         $sth = $dbh->prepare($query);
1107         $sth->execute( $reserve_id );
1108
1109         $query = "
1110             DELETE FROM reserves
1111             WHERE  reserve_id = ?
1112         ";
1113         $sth = $dbh->prepare($query);
1114         $sth->execute( $reserve_id );
1115
1116         # now fix the priority on the others....
1117         _FixPriority({ biblionumber => $reserve->{biblionumber} });
1118     }
1119
1120     return $reserve;
1121 }
1122
1123 =head2 ModReserve
1124
1125   ModReserve({ rank => $rank,
1126                reserve_id => $reserve_id,
1127                branchcode => $branchcode
1128                [, itemnumber => $itemnumber ]
1129                [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
1130               });
1131
1132 Change a hold request's priority or cancel it.
1133
1134 C<$rank> specifies the effect of the change.  If C<$rank>
1135 is 'W' or 'n', nothing happens.  This corresponds to leaving a
1136 request alone when changing its priority in the holds queue
1137 for a bib.
1138
1139 If C<$rank> is 'del', the hold request is cancelled.
1140
1141 If C<$rank> is an integer greater than zero, the priority of
1142 the request is set to that value.  Since priority != 0 means
1143 that the item is not waiting on the hold shelf, setting the 
1144 priority to a non-zero value also sets the request's found
1145 status and waiting date to NULL. 
1146
1147 The optional C<$itemnumber> parameter is used only when
1148 C<$rank> is a non-zero integer; if supplied, the itemnumber 
1149 of the hold request is set accordingly; if omitted, the itemnumber
1150 is cleared.
1151
1152 B<FIXME:> Note that the forgoing can have the effect of causing
1153 item-level hold requests to turn into title-level requests.  This
1154 will be fixed once reserves has separate columns for requested
1155 itemnumber and supplying itemnumber.
1156
1157 =cut
1158
1159 sub ModReserve {
1160     my ( $params ) = @_;
1161
1162     my $rank = $params->{'rank'};
1163     my $reserve_id = $params->{'reserve_id'};
1164     my $branchcode = $params->{'branchcode'};
1165     my $itemnumber = $params->{'itemnumber'};
1166     my $suspend_until = $params->{'suspend_until'};
1167     my $borrowernumber = $params->{'borrowernumber'};
1168     my $biblionumber = $params->{'biblionumber'};
1169
1170     return if $rank eq "W";
1171     return if $rank eq "n";
1172
1173     return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
1174     $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber }) unless ( $reserve_id );
1175
1176     my $dbh = C4::Context->dbh;
1177     if ( $rank eq "del" ) {
1178         CancelReserve({ reserve_id => $reserve_id });
1179     }
1180     elsif ($rank =~ /^\d+/ and $rank > 0) {
1181         my $query = "
1182             UPDATE reserves SET priority = ? ,branchcode = ?, itemnumber = ?, found = NULL, waitingdate = NULL
1183             WHERE reserve_id = ?
1184         ";
1185         my $sth = $dbh->prepare($query);
1186         $sth->execute( $rank, $branchcode, $itemnumber, $reserve_id );
1187
1188         if ( defined( $suspend_until ) ) {
1189             if ( $suspend_until ) {
1190                 $suspend_until = C4::Dates->new( $suspend_until )->output("iso");
1191                 $dbh->do("UPDATE reserves SET suspend = 1, suspend_until = ? WHERE reserve_id = ?", undef, ( $suspend_until, $reserve_id ) );
1192             } else {
1193                 $dbh->do("UPDATE reserves SET suspend_until = NULL WHERE reserve_id = ?", undef, ( $reserve_id ) );
1194             }
1195         }
1196
1197         _FixPriority({ reserve_id => $reserve_id, rank =>$rank });
1198     }
1199 }
1200
1201 =head2 ModReserveFill
1202
1203   &ModReserveFill($reserve);
1204
1205 Fill a reserve. If I understand this correctly, this means that the
1206 reserved book has been found and given to the patron who reserved it.
1207
1208 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
1209 whose keys are fields from the reserves table in the Koha database.
1210
1211 =cut
1212
1213 sub ModReserveFill {
1214     my ($res) = @_;
1215     my $dbh = C4::Context->dbh;
1216     # fill in a reserve record....
1217     my $reserve_id = $res->{'reserve_id'};
1218     my $biblionumber = $res->{'biblionumber'};
1219     my $borrowernumber    = $res->{'borrowernumber'};
1220     my $resdate = $res->{'reservedate'};
1221
1222     # get the priority on this record....
1223     my $priority;
1224     my $query = "SELECT priority
1225                  FROM   reserves
1226                  WHERE  biblionumber   = ?
1227                   AND   borrowernumber = ?
1228                   AND   reservedate    = ?";
1229     my $sth = $dbh->prepare($query);
1230     $sth->execute( $biblionumber, $borrowernumber, $resdate );
1231     ($priority) = $sth->fetchrow_array;
1232
1233     # update the database...
1234     $query = "UPDATE reserves
1235                   SET    found            = 'F',
1236                          priority         = 0
1237                  WHERE  biblionumber     = ?
1238                     AND reservedate      = ?
1239                     AND borrowernumber   = ?
1240                 ";
1241     $sth = $dbh->prepare($query);
1242     $sth->execute( $biblionumber, $resdate, $borrowernumber );
1243
1244     # move to old_reserves
1245     $query = "INSERT INTO old_reserves
1246                  SELECT * FROM reserves
1247                  WHERE  biblionumber     = ?
1248                     AND reservedate      = ?
1249                     AND borrowernumber   = ?
1250                 ";
1251     $sth = $dbh->prepare($query);
1252     $sth->execute( $biblionumber, $resdate, $borrowernumber );
1253     $query = "DELETE FROM reserves
1254                  WHERE  biblionumber     = ?
1255                     AND reservedate      = ?
1256                     AND borrowernumber   = ?
1257                 ";
1258     $sth = $dbh->prepare($query);
1259     $sth->execute( $biblionumber, $resdate, $borrowernumber );
1260     
1261     # now fix the priority on the others (if the priority wasn't
1262     # already sorted!)....
1263     unless ( $priority == 0 ) {
1264         _FixPriority({ reserve_id => $reserve_id });
1265     }
1266 }
1267
1268 =head2 ModReserveStatus
1269
1270   &ModReserveStatus($itemnumber, $newstatus);
1271
1272 Update the reserve status for the active (priority=0) reserve.
1273
1274 $itemnumber is the itemnumber the reserve is on
1275
1276 $newstatus is the new status.
1277
1278 =cut
1279
1280 sub ModReserveStatus {
1281
1282     #first : check if we have a reservation for this item .
1283     my ($itemnumber, $newstatus) = @_;
1284     my $dbh = C4::Context->dbh;
1285
1286     my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1287     my $sth_set = $dbh->prepare($query);
1288     $sth_set->execute( $newstatus, $itemnumber );
1289
1290     if ( C4::Context->preference("ReturnToShelvingCart") && $newstatus ) {
1291       CartToShelf( $itemnumber );
1292     }
1293 }
1294
1295 =head2 ModReserveAffect
1296
1297   &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend);
1298
1299 This function affect an item and a status for a given reserve
1300 The itemnumber parameter is used to find the biblionumber.
1301 with the biblionumber & the borrowernumber, we can affect the itemnumber
1302 to the correct reserve.
1303
1304 if $transferToDo is not set, then the status is set to "Waiting" as well.
1305 otherwise, a transfer is on the way, and the end of the transfer will 
1306 take care of the waiting status
1307
1308 =cut
1309
1310 sub ModReserveAffect {
1311     my ( $itemnumber, $borrowernumber,$transferToDo ) = @_;
1312     my $dbh = C4::Context->dbh;
1313
1314     # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1315     # attached to $itemnumber
1316     my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1317     $sth->execute($itemnumber);
1318     my ($biblionumber) = $sth->fetchrow;
1319
1320     # get request - need to find out if item is already
1321     # waiting in order to not send duplicate hold filled notifications
1322     my $reserve_id = GetReserveId({
1323         borrowernumber => $borrowernumber,
1324         biblionumber   => $biblionumber,
1325     });
1326     return unless defined $reserve_id;
1327     my $request = GetReserveInfo($reserve_id);
1328     my $already_on_shelf = ($request && $request->{found} eq 'W') ? 1 : 0;
1329
1330     # If we affect a reserve that has to be transfered, don't set to Waiting
1331     my $query;
1332     if ($transferToDo) {
1333     $query = "
1334         UPDATE reserves
1335         SET    priority = 0,
1336                itemnumber = ?,
1337                found = 'T'
1338         WHERE borrowernumber = ?
1339           AND biblionumber = ?
1340     ";
1341     }
1342     else {
1343     # affect the reserve to Waiting as well.
1344         $query = "
1345             UPDATE reserves
1346             SET     priority = 0,
1347                     found = 'W',
1348                     waitingdate = NOW(),
1349                     itemnumber = ?
1350             WHERE borrowernumber = ?
1351               AND biblionumber = ?
1352         ";
1353     }
1354     $sth = $dbh->prepare($query);
1355     $sth->execute( $itemnumber, $borrowernumber,$biblionumber);
1356     _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber ) if ( !$transferToDo && !$already_on_shelf );
1357     _FixPriority( { biblionumber => $biblionumber } );
1358     if ( C4::Context->preference("ReturnToShelvingCart") ) {
1359       CartToShelf( $itemnumber );
1360     }
1361
1362     return;
1363 }
1364
1365 =head2 ModReserveCancelAll
1366
1367   ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1368
1369 function to cancel reserv,check other reserves, and transfer document if it's necessary
1370
1371 =cut
1372
1373 sub ModReserveCancelAll {
1374     my $messages;
1375     my $nextreservinfo;
1376     my ( $itemnumber, $borrowernumber ) = @_;
1377
1378     #step 1 : cancel the reservation
1379     my $CancelReserve = CancelReserve({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1380
1381     #step 2 launch the subroutine of the others reserves
1382     ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1383
1384     return ( $messages, $nextreservinfo );
1385 }
1386
1387 =head2 ModReserveMinusPriority
1388
1389   &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1390
1391 Reduce the values of queued list
1392
1393 =cut
1394
1395 sub ModReserveMinusPriority {
1396     my ( $itemnumber, $reserve_id ) = @_;
1397
1398     #first step update the value of the first person on reserv
1399     my $dbh   = C4::Context->dbh;
1400     my $query = "
1401         UPDATE reserves
1402         SET    priority = 0 , itemnumber = ? 
1403         WHERE  reserve_id = ?
1404     ";
1405     my $sth_upd = $dbh->prepare($query);
1406     $sth_upd->execute( $itemnumber, $reserve_id );
1407     # second step update all others reserves
1408     _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1409 }
1410
1411 =head2 GetReserveInfo
1412
1413   &GetReserveInfo($reserve_id);
1414
1415 Get item and borrower details for a current hold.
1416 Current implementation this query should have a single result.
1417
1418 =cut
1419
1420 sub GetReserveInfo {
1421     my ( $reserve_id ) = @_;
1422     my $dbh = C4::Context->dbh;
1423     my $strsth="SELECT
1424                    reserve_id,
1425                    reservedate,
1426                    reservenotes,
1427                    reserves.borrowernumber,
1428                    reserves.biblionumber,
1429                    reserves.branchcode,
1430                    reserves.waitingdate,
1431                    notificationdate,
1432                    reminderdate,
1433                    priority,
1434                    found,
1435                    firstname,
1436                    surname,
1437                    phone,
1438                    email,
1439                    address,
1440                    address2,
1441                    cardnumber,
1442                    city,
1443                    zipcode,
1444                    biblio.title,
1445                    biblio.author,
1446                    items.holdingbranch,
1447                    items.itemcallnumber,
1448                    items.itemnumber,
1449                    items.location,
1450                    barcode,
1451                    notes
1452                 FROM reserves
1453                 LEFT JOIN items USING(itemnumber)
1454                 LEFT JOIN borrowers USING(borrowernumber)
1455                 LEFT JOIN biblio ON  (reserves.biblionumber=biblio.biblionumber)
1456                 WHERE reserves.reserve_id = ?";
1457     my $sth = $dbh->prepare($strsth);
1458     $sth->execute($reserve_id);
1459
1460     my $data = $sth->fetchrow_hashref;
1461     return $data;
1462 }
1463
1464 =head2 IsAvailableForItemLevelRequest
1465
1466   my $is_available = IsAvailableForItemLevelRequest($itemnumber);
1467
1468 Checks whether a given item record is available for an
1469 item-level hold request.  An item is available if
1470
1471 * it is not lost AND 
1472 * it is not damaged AND 
1473 * it is not withdrawn AND 
1474 * does not have a not for loan value > 0
1475
1476 Whether or not the item is currently on loan is 
1477 also checked - if the AllowOnShelfHolds system preference
1478 is ON, an item can be requested even if it is currently
1479 on loan to somebody else.  If the system preference
1480 is OFF, an item that is currently checked out cannot
1481 be the target of an item-level hold request.
1482
1483 Note that IsAvailableForItemLevelRequest() does not
1484 check if the staff operator is authorized to place
1485 a request on the item - in particular,
1486 this routine does not check IndependentBranches
1487 and canreservefromotherbranches.
1488
1489 =cut
1490
1491 sub IsAvailableForItemLevelRequest {
1492     my $itemnumber = shift;
1493    
1494     my $item = GetItem($itemnumber);
1495
1496     # must check the notforloan setting of the itemtype
1497     # FIXME - a lot of places in the code do this
1498     #         or something similar - need to be
1499     #         consolidated
1500     my $dbh = C4::Context->dbh;
1501     my $notforloan_query;
1502     if (C4::Context->preference('item-level_itypes')) {
1503         $notforloan_query = "SELECT itemtypes.notforloan
1504                              FROM items
1505                              JOIN itemtypes ON (itemtypes.itemtype = items.itype)
1506                              WHERE itemnumber = ?";
1507     } else {
1508         $notforloan_query = "SELECT itemtypes.notforloan
1509                              FROM items
1510                              JOIN biblioitems USING (biblioitemnumber)
1511                              JOIN itemtypes USING (itemtype)
1512                              WHERE itemnumber = ?";
1513     }
1514     my $sth = $dbh->prepare($notforloan_query);
1515     $sth->execute($itemnumber);
1516     my $notforloan_per_itemtype = 0;
1517     if (my ($notforloan) = $sth->fetchrow_array) {
1518         $notforloan_per_itemtype = 1 if $notforloan;
1519     }
1520
1521     my $available_per_item = 1;
1522     $available_per_item = 0 if $item->{itemlost} or
1523                                ( $item->{notforloan} > 0 ) or
1524                                ($item->{damaged} and not C4::Context->preference('AllowHoldsOnDamagedItems')) or
1525                                $item->{withdrawn} or
1526                                $notforloan_per_itemtype;
1527
1528
1529     if (C4::Context->preference('AllowOnShelfHolds')) {
1530         return $available_per_item;
1531     } else {
1532         return ($available_per_item and ($item->{onloan} or GetReserveStatus($itemnumber) eq "Waiting"));
1533     }
1534 }
1535
1536 =head2 AlterPriority
1537
1538   AlterPriority( $where, $reserve_id );
1539
1540 This function changes a reserve's priority up, down, to the top, or to the bottom.
1541 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1542
1543 =cut
1544
1545 sub AlterPriority {
1546     my ( $where, $reserve_id ) = @_;
1547
1548     my $dbh = C4::Context->dbh;
1549
1550     my $reserve = GetReserve( $reserve_id );
1551
1552     if ( $reserve->{cancellationdate} ) {
1553         warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (".$reserve->{cancellationdate}.')';
1554         return;
1555     }
1556
1557     if ( $where eq 'up' || $where eq 'down' ) {
1558
1559       my $priority = $reserve->{'priority'};
1560       $priority = $where eq 'up' ? $priority - 1 : $priority + 1;
1561       _FixPriority({ reserve_id => $reserve_id, rank => $priority })
1562
1563     } elsif ( $where eq 'top' ) {
1564
1565       _FixPriority({ reserve_id => $reserve_id, rank => '1' })
1566
1567     } elsif ( $where eq 'bottom' ) {
1568
1569       _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1570
1571     }
1572 }
1573
1574 =head2 ToggleLowestPriority
1575
1576   ToggleLowestPriority( $borrowernumber, $biblionumber );
1577
1578 This function sets the lowestPriority field to true if is false, and false if it is true.
1579
1580 =cut
1581
1582 sub ToggleLowestPriority {
1583     my ( $reserve_id ) = @_;
1584
1585     my $dbh = C4::Context->dbh;
1586
1587     my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1588     $sth->execute( $reserve_id );
1589     
1590     _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1591 }
1592
1593 =head2 ToggleSuspend
1594
1595   ToggleSuspend( $reserve_id );
1596
1597 This function sets the suspend field to true if is false, and false if it is true.
1598 If the reserve is currently suspended with a suspend_until date, that date will
1599 be cleared when it is unsuspended.
1600
1601 =cut
1602
1603 sub ToggleSuspend {
1604     my ( $reserve_id, $suspend_until ) = @_;
1605
1606     $suspend_until = output_pref(
1607         {
1608             dt         => dt_from_string($suspend_until),
1609             dateformat => 'iso',
1610             dateonly   => 1
1611         }
1612     ) if ($suspend_until);
1613
1614     my $do_until = ( $suspend_until ) ? '?' : 'NULL';
1615
1616     my $dbh = C4::Context->dbh;
1617
1618     my $sth = $dbh->prepare(
1619         "UPDATE reserves SET suspend = NOT suspend,
1620         suspend_until = CASE WHEN suspend = 0 THEN NULL ELSE $do_until END
1621         WHERE reserve_id = ?
1622     ");
1623
1624     my @params;
1625     push( @params, $suspend_until ) if ( $suspend_until );
1626     push( @params, $reserve_id );
1627
1628     $sth->execute( @params );
1629 }
1630
1631 =head2 SuspendAll
1632
1633   SuspendAll(
1634       borrowernumber   => $borrowernumber,
1635       [ biblionumber   => $biblionumber, ]
1636       [ suspend_until  => $suspend_until, ]
1637       [ suspend        => $suspend ]
1638   );
1639
1640   This function accepts a set of hash keys as its parameters.
1641   It requires either borrowernumber or biblionumber, or both.
1642
1643   suspend_until is wholly optional.
1644
1645 =cut
1646
1647 sub SuspendAll {
1648     my %params = @_;
1649
1650     my $borrowernumber = $params{'borrowernumber'} || undef;
1651     my $biblionumber   = $params{'biblionumber'}   || undef;
1652     my $suspend_until  = $params{'suspend_until'}  || undef;
1653     my $suspend        = defined( $params{'suspend'} ) ? $params{'suspend'} :  1;
1654
1655     $suspend_until = C4::Dates->new( $suspend_until )->output("iso") if ( defined( $suspend_until ) );
1656
1657     return unless ( $borrowernumber || $biblionumber );
1658
1659     my ( $query, $sth, $dbh, @query_params );
1660
1661     $query = "UPDATE reserves SET suspend = ? ";
1662     push( @query_params, $suspend );
1663     if ( !$suspend ) {
1664         $query .= ", suspend_until = NULL ";
1665     } elsif ( $suspend_until ) {
1666         $query .= ", suspend_until = ? ";
1667         push( @query_params, $suspend_until );
1668     }
1669     $query .= " WHERE ";
1670     if ( $borrowernumber ) {
1671         $query .= " borrowernumber = ? ";
1672         push( @query_params, $borrowernumber );
1673     }
1674     $query .= " AND " if ( $borrowernumber && $biblionumber );
1675     if ( $biblionumber ) {
1676         $query .= " biblionumber = ? ";
1677         push( @query_params, $biblionumber );
1678     }
1679     $query .= " AND found IS NULL ";
1680
1681     $dbh = C4::Context->dbh;
1682     $sth = $dbh->prepare( $query );
1683     $sth->execute( @query_params );
1684 }
1685
1686
1687 =head2 _FixPriority
1688
1689   _FixPriority({
1690     reserve_id => $reserve_id,
1691     [rank => $rank,]
1692     [ignoreSetLowestRank => $ignoreSetLowestRank]
1693   });
1694
1695   or
1696
1697   _FixPriority({ biblionumber => $biblionumber});
1698
1699 This routine adjusts the priority of a hold request and holds
1700 on the same bib.
1701
1702 In the first form, where a reserve_id is passed, the priority of the
1703 hold is set to supplied rank, and other holds for that bib are adjusted
1704 accordingly.  If the rank is "del", the hold is cancelled.  If no rank
1705 is supplied, all of the holds on that bib have their priority adjusted
1706 as if the second form had been used.
1707
1708 In the second form, where a biblionumber is passed, the holds on that
1709 bib (that are not captured) are sorted in order of increasing priority,
1710 then have reserves.priority set so that the first non-captured hold
1711 has its priority set to 1, the second non-captured hold has its priority
1712 set to 2, and so forth.
1713
1714 In both cases, holds that have the lowestPriority flag on are have their
1715 priority adjusted to ensure that they remain at the end of the line.
1716
1717 Note that the ignoreSetLowestRank parameter is meant to be used only
1718 when _FixPriority calls itself.
1719
1720 =cut
1721
1722 sub _FixPriority {
1723     my ( $params ) = @_;
1724     my $reserve_id = $params->{reserve_id};
1725     my $rank = $params->{rank} // '';
1726     my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1727     my $biblionumber = $params->{biblionumber};
1728
1729     my $dbh = C4::Context->dbh;
1730
1731     unless ( $biblionumber ) {
1732         my $res = GetReserve( $reserve_id );
1733         $biblionumber = $res->{biblionumber};
1734     }
1735
1736     if ( $rank eq "del" ) {
1737          CancelReserve({ reserve_id => $reserve_id });
1738     }
1739     elsif ( $rank eq "W" || $rank eq "0" ) {
1740
1741         # make sure priority for waiting or in-transit items is 0
1742         my $query = "
1743             UPDATE reserves
1744             SET    priority = 0
1745             WHERE reserve_id = ?
1746             AND found IN ('W', 'T')
1747         ";
1748         my $sth = $dbh->prepare($query);
1749         $sth->execute( $reserve_id );
1750     }
1751     my @priority;
1752
1753     # get whats left
1754     my $query = "
1755         SELECT reserve_id, borrowernumber, reservedate, constrainttype
1756         FROM   reserves
1757         WHERE  biblionumber   = ?
1758           AND  ((found <> 'W' AND found <> 'T') OR found IS NULL)
1759         ORDER BY priority ASC
1760     ";
1761     my $sth = $dbh->prepare($query);
1762     $sth->execute( $biblionumber );
1763     while ( my $line = $sth->fetchrow_hashref ) {
1764         push( @priority,     $line );
1765     }
1766
1767     # To find the matching index
1768     my $i;
1769     my $key = -1;    # to allow for 0 to be a valid result
1770     for ( $i = 0 ; $i < @priority ; $i++ ) {
1771         if ( $reserve_id == $priority[$i]->{'reserve_id'} ) {
1772             $key = $i;    # save the index
1773             last;
1774         }
1775     }
1776
1777     # if index exists in array then move it to new position
1778     if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1779         my $new_rank = $rank -
1780           1;    # $new_rank is what you want the new index to be in the array
1781         my $moving_item = splice( @priority, $key, 1 );
1782         splice( @priority, $new_rank, 0, $moving_item );
1783     }
1784
1785     # now fix the priority on those that are left....
1786     $query = "
1787         UPDATE reserves
1788         SET    priority = ?
1789         WHERE  reserve_id = ?
1790     ";
1791     $sth = $dbh->prepare($query);
1792     for ( my $j = 0 ; $j < @priority ; $j++ ) {
1793         $sth->execute(
1794             $j + 1,
1795             $priority[$j]->{'reserve_id'}
1796         );
1797     }
1798     
1799     $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1800     $sth->execute();
1801     
1802     unless ( $ignoreSetLowestRank ) {
1803       while ( my $res = $sth->fetchrow_hashref() ) {
1804         _FixPriority({
1805             reserve_id => $res->{'reserve_id'},
1806             rank => '999999',
1807             ignoreSetLowestRank => 1
1808         });
1809       }
1810     }
1811 }
1812
1813 =head2 _Findgroupreserve
1814
1815   @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead);
1816
1817 Looks for an item-specific match first, then for a title-level match, returning the
1818 first match found.  If neither, then we look for a 3rd kind of match based on
1819 reserve constraints.
1820 Lookahead is the number of days to look in advance.
1821
1822 TODO: add more explanation about reserve constraints
1823
1824 C<&_Findgroupreserve> returns :
1825 C<@results> is an array of references-to-hash whose keys are mostly
1826 fields from the reserves table of the Koha database, plus
1827 C<biblioitemnumber>.
1828
1829 =cut
1830
1831 sub _Findgroupreserve {
1832     my ( $bibitem, $biblio, $itemnumber, $lookahead) = @_;
1833     my $dbh   = C4::Context->dbh;
1834
1835     # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1836     # check for exact targetted match
1837     my $item_level_target_query = qq/
1838         SELECT reserves.biblionumber        AS biblionumber,
1839                reserves.borrowernumber      AS borrowernumber,
1840                reserves.reservedate         AS reservedate,
1841                reserves.branchcode          AS branchcode,
1842                reserves.cancellationdate    AS cancellationdate,
1843                reserves.found               AS found,
1844                reserves.reservenotes        AS reservenotes,
1845                reserves.priority            AS priority,
1846                reserves.timestamp           AS timestamp,
1847                biblioitems.biblioitemnumber AS biblioitemnumber,
1848                reserves.itemnumber          AS itemnumber,
1849                reserves.reserve_id          AS reserve_id
1850         FROM reserves
1851         JOIN biblioitems USING (biblionumber)
1852         JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1853         WHERE found IS NULL
1854         AND priority > 0
1855         AND item_level_request = 1
1856         AND itemnumber = ?
1857         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1858         AND suspend = 0
1859     /;
1860     my $sth = $dbh->prepare($item_level_target_query);
1861     $sth->execute($itemnumber, $lookahead||0);
1862     my @results;
1863     if ( my $data = $sth->fetchrow_hashref ) {
1864         push( @results, $data );
1865     }
1866     return @results if @results;
1867     
1868     # check for title-level targetted match
1869     my $title_level_target_query = qq/
1870         SELECT reserves.biblionumber        AS biblionumber,
1871                reserves.borrowernumber      AS borrowernumber,
1872                reserves.reservedate         AS reservedate,
1873                reserves.branchcode          AS branchcode,
1874                reserves.cancellationdate    AS cancellationdate,
1875                reserves.found               AS found,
1876                reserves.reservenotes        AS reservenotes,
1877                reserves.priority            AS priority,
1878                reserves.timestamp           AS timestamp,
1879                biblioitems.biblioitemnumber AS biblioitemnumber,
1880                reserves.itemnumber          AS itemnumber,
1881                reserves.reserve_id          AS reserve_id
1882         FROM reserves
1883         JOIN biblioitems USING (biblionumber)
1884         JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1885         WHERE found IS NULL
1886         AND priority > 0
1887         AND item_level_request = 0
1888         AND hold_fill_targets.itemnumber = ?
1889         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1890         AND suspend = 0
1891     /;
1892     $sth = $dbh->prepare($title_level_target_query);
1893     $sth->execute($itemnumber, $lookahead||0);
1894     @results = ();
1895     if ( my $data = $sth->fetchrow_hashref ) {
1896         push( @results, $data );
1897     }
1898     return @results if @results;
1899
1900     my $query = qq/
1901         SELECT reserves.biblionumber               AS biblionumber,
1902                reserves.borrowernumber             AS borrowernumber,
1903                reserves.reservedate                AS reservedate,
1904                reserves.waitingdate                AS waitingdate,
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                reserveconstraints.biblioitemnumber AS biblioitemnumber,
1912                reserves.itemnumber                 AS itemnumber,
1913                reserves.reserve_id                 AS reserve_id
1914         FROM reserves
1915           LEFT JOIN reserveconstraints ON reserves.biblionumber = reserveconstraints.biblionumber
1916         WHERE reserves.biblionumber = ?
1917           AND ( ( reserveconstraints.biblioitemnumber = ?
1918           AND reserves.borrowernumber = reserveconstraints.borrowernumber
1919           AND reserves.reservedate    = reserveconstraints.reservedate )
1920           OR  reserves.constrainttype='a' )
1921           AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1922           AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1923           AND suspend = 0
1924     /;
1925     $sth = $dbh->prepare($query);
1926     $sth->execute( $biblio, $bibitem, $itemnumber, $lookahead||0);
1927     @results = ();
1928     while ( my $data = $sth->fetchrow_hashref ) {
1929         push( @results, $data );
1930     }
1931     return @results;
1932 }
1933
1934 =head2 _koha_notify_reserve
1935
1936   _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber );
1937
1938 Sends a notification to the patron that their hold has been filled (through
1939 ModReserveAffect, _not_ ModReserveFill)
1940
1941 =cut
1942
1943 sub _koha_notify_reserve {
1944     my ($itemnumber, $borrowernumber, $biblionumber) = @_;
1945
1946     my $dbh = C4::Context->dbh;
1947     my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
1948     
1949     # Try to get the borrower's email address
1950     my $to_address = C4::Members::GetNoticeEmailAddress($borrowernumber);
1951
1952     my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
1953             borrowernumber => $borrowernumber,
1954             message_name => 'Hold_Filled'
1955     } );
1956
1957     my $sth = $dbh->prepare("
1958         SELECT *
1959         FROM   reserves
1960         WHERE  borrowernumber = ?
1961             AND biblionumber = ?
1962     ");
1963     $sth->execute( $borrowernumber, $biblionumber );
1964     my $reserve = $sth->fetchrow_hashref;
1965     my $branch_details = GetBranchDetail( $reserve->{'branchcode'} );
1966
1967     my $admin_email_address = $branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
1968
1969     my %letter_params = (
1970         module => 'reserves',
1971         branchcode => $reserve->{branchcode},
1972         tables => {
1973             'branches'  => $branch_details,
1974             'borrowers' => $borrower,
1975             'biblio'    => $biblionumber,
1976             'reserves'  => $reserve,
1977             'items', $reserve->{'itemnumber'},
1978         },
1979         substitute => { today => C4::Dates->new()->output() },
1980     );
1981
1982     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.
1983     my $send_notification = sub {
1984         my ( $mtt, $letter_code ) = (@_);
1985         return unless defined $letter_code;
1986         $letter_params{letter_code} = $letter_code;
1987         $letter_params{message_transport_type} = $mtt;
1988         my $letter =  C4::Letters::GetPreparedLetter ( %letter_params );
1989         unless ($letter) {
1990             warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
1991             return;
1992         }
1993
1994         C4::Letters::EnqueueLetter( {
1995             letter => $letter,
1996             borrowernumber => $borrowernumber,
1997             from_address => $admin_email_address,
1998             message_transport_type => $mtt,
1999         } );
2000     };
2001
2002     while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
2003         if ( ($mtt eq 'email' and not $to_address) or ($mtt eq 'sms' and not $borrower->{smsalertnumber}) ) {
2004             # email or sms is requested but not exist
2005             next;
2006         }
2007         &$send_notification($mtt, $letter_code);
2008         $notification_sent++;
2009     }
2010     #Making sure that a print notification is sent if no other transport types can be utilized.
2011     if (! $notification_sent) {
2012         &$send_notification('print', 'HOLD');
2013     }
2014     
2015 }
2016
2017 =head2 _ShiftPriorityByDateAndPriority
2018
2019   $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
2020
2021 This increments the priority of all reserves after the one
2022 with either the lowest date after C<$reservedate>
2023 or the lowest priority after C<$priority>.
2024
2025 It effectively makes room for a new reserve to be inserted with a certain
2026 priority, which is returned.
2027
2028 This is most useful when the reservedate can be set by the user.  It allows
2029 the new reserve to be placed before other reserves that have a later
2030 reservedate.  Since priority also is set by the form in reserves/request.pl
2031 the sub accounts for that too.
2032
2033 =cut
2034
2035 sub _ShiftPriorityByDateAndPriority {
2036     my ( $biblio, $resdate, $new_priority ) = @_;
2037
2038     my $dbh = C4::Context->dbh;
2039     my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
2040     my $sth = $dbh->prepare( $query );
2041     $sth->execute( $biblio, $resdate, $new_priority );
2042     my $min_priority = $sth->fetchrow;
2043     # if no such matches are found, $new_priority remains as original value
2044     $new_priority = $min_priority if ( $min_priority );
2045
2046     # Shift the priority up by one; works in conjunction with the next SQL statement
2047     $query = "UPDATE reserves
2048               SET priority = priority+1
2049               WHERE biblionumber = ?
2050               AND borrowernumber = ?
2051               AND reservedate = ?
2052               AND found IS NULL";
2053     my $sth_update = $dbh->prepare( $query );
2054
2055     # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
2056     $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
2057     $sth = $dbh->prepare( $query );
2058     $sth->execute( $new_priority, $biblio );
2059     while ( my $row = $sth->fetchrow_hashref ) {
2060         $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
2061     }
2062
2063     return $new_priority;  # so the caller knows what priority they wind up receiving
2064 }
2065
2066 =head2 MoveReserve
2067
2068   MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
2069
2070 Use when checking out an item to handle reserves
2071 If $cancelreserve boolean is set to true, it will remove existing reserve
2072
2073 =cut
2074
2075 sub MoveReserve {
2076     my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
2077
2078     my ( $restype, $res, $all_reserves ) = CheckReserves( $itemnumber );
2079     return unless $res;
2080
2081     my $biblionumber     =  $res->{biblionumber};
2082     my $biblioitemnumber = $res->{biblioitemnumber};
2083
2084     if ($res->{borrowernumber} == $borrowernumber) {
2085         ModReserveFill($res);
2086     }
2087     else {
2088         # warn "Reserved";
2089         # The item is reserved by someone else.
2090         # Find this item in the reserves
2091
2092         my $borr_res;
2093         foreach (@$all_reserves) {
2094             $_->{'borrowernumber'} == $borrowernumber or next;
2095             $_->{'biblionumber'}   == $biblionumber   or next;
2096
2097             $borr_res = $_;
2098             last;
2099         }
2100
2101         if ( $borr_res ) {
2102             # The item is reserved by the current patron
2103             ModReserveFill($borr_res);
2104         }
2105
2106         if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
2107             RevertWaitingStatus({ itemnumber => $itemnumber });
2108         }
2109         elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
2110             CancelReserve({
2111                 biblionumber   => $res->{'biblionumber'},
2112                 itemnumber     => $res->{'itemnumber'},
2113                 borrowernumber => $res->{'borrowernumber'}
2114             });
2115         }
2116     }
2117 }
2118
2119 =head2 MergeHolds
2120
2121   MergeHolds($dbh,$to_biblio, $from_biblio);
2122
2123 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
2124
2125 =cut
2126
2127 sub MergeHolds {
2128     my ( $dbh, $to_biblio, $from_biblio ) = @_;
2129     my $sth = $dbh->prepare(
2130         "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
2131     );
2132     $sth->execute($from_biblio);
2133     if ( my $data = $sth->fetchrow_hashref() ) {
2134
2135         # holds exist on old record, if not we don't need to do anything
2136         $sth = $dbh->prepare(
2137             "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
2138         $sth->execute( $to_biblio, $from_biblio );
2139
2140         # Reorder by date
2141         # don't reorder those already waiting
2142
2143         $sth = $dbh->prepare(
2144 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
2145         );
2146         my $upd_sth = $dbh->prepare(
2147 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
2148         AND reservedate = ? AND constrainttype = ? AND (itemnumber = ? or itemnumber is NULL) "
2149         );
2150         $sth->execute( $to_biblio, 'W', 'T' );
2151         my $priority = 1;
2152         while ( my $reserve = $sth->fetchrow_hashref() ) {
2153             $upd_sth->execute(
2154                 $priority,                    $to_biblio,
2155                 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
2156                 $reserve->{'constrainttype'}, $reserve->{'itemnumber'}
2157             );
2158             $priority++;
2159         }
2160     }
2161 }
2162
2163 =head2 RevertWaitingStatus
2164
2165   RevertWaitingStatus({ itemnumber => $itemnumber });
2166
2167   Reverts a 'waiting' hold back to a regular hold with a priority of 1.
2168
2169   Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
2170           item level hold, even if it was only a bibliolevel hold to
2171           begin with. This is because we can no longer know if a hold
2172           was item-level or bib-level after a hold has been set to
2173           waiting status.
2174
2175 =cut
2176
2177 sub RevertWaitingStatus {
2178     my ( $params ) = @_;
2179     my $itemnumber = $params->{'itemnumber'};
2180
2181     return unless ( $itemnumber );
2182
2183     my $dbh = C4::Context->dbh;
2184
2185     ## Get the waiting reserve we want to revert
2186     my $query = "
2187         SELECT * FROM reserves
2188         WHERE itemnumber = ?
2189         AND found IS NOT NULL
2190     ";
2191     my $sth = $dbh->prepare( $query );
2192     $sth->execute( $itemnumber );
2193     my $reserve = $sth->fetchrow_hashref();
2194
2195     ## Increment the priority of all other non-waiting
2196     ## reserves for this bib record
2197     $query = "
2198         UPDATE reserves
2199         SET
2200           priority = priority + 1
2201         WHERE
2202           biblionumber =  ?
2203         AND
2204           priority > 0
2205     ";
2206     $sth = $dbh->prepare( $query );
2207     $sth->execute( $reserve->{'biblionumber'} );
2208
2209     ## Fix up the currently waiting reserve
2210     $query = "
2211     UPDATE reserves
2212     SET
2213       priority = 1,
2214       found = NULL,
2215       waitingdate = NULL
2216     WHERE
2217       reserve_id = ?
2218     ";
2219     $sth = $dbh->prepare( $query );
2220     $sth->execute( $reserve->{'reserve_id'} );
2221     _FixPriority( { biblionumber => $reserve->{biblionumber} } );
2222 }
2223
2224 =head2 GetReserveId
2225
2226   $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber [, itemnumber => $itemnumber ] });
2227
2228   Returnes the first reserve id that matches the given criteria
2229
2230 =cut
2231
2232 sub GetReserveId {
2233     my ( $params ) = @_;
2234
2235     return unless ( ( $params->{'biblionumber'} || $params->{'itemnumber'} ) && $params->{'borrowernumber'} );
2236
2237     my $dbh = C4::Context->dbh();
2238
2239     my $sql = "SELECT reserve_id FROM reserves WHERE ";
2240
2241     my @params;
2242     my @limits;
2243     foreach my $key ( keys %$params ) {
2244         if ( defined( $params->{$key} ) ) {
2245             push( @limits, "$key = ?" );
2246             push( @params, $params->{$key} );
2247         }
2248     }
2249
2250     $sql .= join( " AND ", @limits );
2251
2252     my $sth = $dbh->prepare( $sql );
2253     $sth->execute( @params );
2254     my $row = $sth->fetchrow_hashref();
2255
2256     return $row->{'reserve_id'};
2257 }
2258
2259 =head2 ReserveSlip
2260
2261   ReserveSlip($branchcode, $borrowernumber, $biblionumber)
2262
2263   Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
2264
2265 =cut
2266
2267 sub ReserveSlip {
2268     my ($branch, $borrowernumber, $biblionumber) = @_;
2269
2270 #   return unless ( C4::Context->boolean_preference('printreserveslips') );
2271
2272     my $reserve_id = GetReserveId({
2273         biblionumber => $biblionumber,
2274         borrowernumber => $borrowernumber
2275     }) or return;
2276     my $reserve = GetReserveInfo($reserve_id) or return;
2277
2278     return  C4::Letters::GetPreparedLetter (
2279         module => 'circulation',
2280         letter_code => 'RESERVESLIP',
2281         branchcode => $branch,
2282         tables => {
2283             'reserves'    => $reserve,
2284             'branches'    => $reserve->{branchcode},
2285             'borrowers'   => $reserve->{borrowernumber},
2286             'biblio'      => $reserve->{biblionumber},
2287             'items'       => $reserve->{itemnumber},
2288         },
2289     );
2290 }
2291
2292 =head2 GetReservesControlBranch
2293
2294   my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
2295
2296   Return the branchcode to be used to determine which reserves
2297   policy applies to a transaction.
2298
2299   C<$item> is a hashref for an item. Only 'homebranch' is used.
2300
2301   C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
2302
2303 =cut
2304
2305 sub GetReservesControlBranch {
2306     my ( $item, $borrower ) = @_;
2307
2308     my $reserves_control = C4::Context->preference('ReservesControlBranch');
2309
2310     my $branchcode =
2311         ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
2312       : ( $reserves_control eq 'PatronLibrary' )   ? $borrower->{'branchcode'}
2313       :                                              undef;
2314
2315     return $branchcode;
2316 }
2317
2318 =head2 CalculatePriority
2319
2320     my $p = CalculatePriority($biblionumber, $resdate);
2321
2322 Calculate priority for a new reserve on biblionumber, placing it at
2323 the end of the line of all holds whose start date falls before
2324 the current system time and that are neither on the hold shelf
2325 or in transit.
2326
2327 The reserve date parameter is optional; if it is supplied, the
2328 priority is based on the set of holds whose start date falls before
2329 the parameter value.
2330
2331 After calculation of this priority, it is recommended to call
2332 _ShiftPriorityByDateAndPriority. Note that this is currently done in
2333 AddReserves.
2334
2335 =cut
2336
2337 sub CalculatePriority {
2338     my ( $biblionumber, $resdate ) = @_;
2339
2340     my $sql = q{
2341         SELECT COUNT(*) FROM reserves
2342         WHERE biblionumber = ?
2343         AND   priority > 0
2344         AND   (found IS NULL OR found = '')
2345     };
2346     #skip found==W or found==T (waiting or transit holds)
2347     if( $resdate ) {
2348         $sql.= ' AND ( reservedate <= ? )';
2349     }
2350     else {
2351         $sql.= ' AND ( reservedate < NOW() )';
2352     }
2353     my $dbh = C4::Context->dbh();
2354     my @row = $dbh->selectrow_array(
2355         $sql,
2356         undef,
2357         $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2358     );
2359
2360     return @row ? $row[0]+1 : 1;
2361 }
2362
2363 =head1 AUTHOR
2364
2365 Koha Development Team <http://koha-community.org/>
2366
2367 =cut
2368
2369 1;