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