Bug 9394: Use reserve_id where possible
[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 ( $where eq 'up' || $where eq 'down' ) {
1470     
1471       my $priority = $reserve->{'priority'};        
1472       $priority = $where eq 'up' ? $priority - 1 : $priority + 1;
1473       _FixPriority( $reserve_id, $priority )
1474
1475     } elsif ( $where eq 'top' ) {
1476
1477       _FixPriority( $reserve_id, '1' )
1478
1479     } elsif ( $where eq 'bottom' ) {
1480
1481       _FixPriority( $reserve_id, '999999' )
1482
1483     }
1484 }
1485
1486 =head2 ToggleLowestPriority
1487
1488   ToggleLowestPriority( $borrowernumber, $biblionumber );
1489
1490 This function sets the lowestPriority field to true if is false, and false if it is true.
1491
1492 =cut
1493
1494 sub ToggleLowestPriority {
1495     my ( $reserve_id ) = @_;
1496
1497     my $dbh = C4::Context->dbh;
1498
1499     my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1500     $sth->execute( $reserve_id );
1501     $sth->finish;
1502     
1503     _FixPriority( $reserve_id, '999999' );
1504 }
1505
1506 =head2 ToggleSuspend
1507
1508   ToggleSuspend( $reserve_id );
1509
1510 This function sets the suspend field to true if is false, and false if it is true.
1511 If the reserve is currently suspended with a suspend_until date, that date will
1512 be cleared when it is unsuspended.
1513
1514 =cut
1515
1516 sub ToggleSuspend {
1517     my ( $reserve_id, $suspend_until ) = @_;
1518
1519     $suspend_until = output_pref( dt_from_string( $suspend_until ), 'iso' ) if ( $suspend_until );
1520
1521     my $do_until = ( $suspend_until ) ? '?' : 'NULL';
1522
1523     my $dbh = C4::Context->dbh;
1524
1525     my $sth = $dbh->prepare(
1526         "UPDATE reserves SET suspend = NOT suspend,
1527         suspend_until = CASE WHEN suspend = 0 THEN NULL ELSE $do_until END
1528         WHERE reserve_id = ?
1529     ");
1530
1531     my @params;
1532     push( @params, $suspend_until ) if ( $suspend_until );
1533     push( @params, $reserve_id );
1534
1535     $sth->execute( @params );
1536     $sth->finish;
1537 }
1538
1539 =head2 SuspendAll
1540
1541   SuspendAll(
1542       borrowernumber   => $borrowernumber,
1543       [ biblionumber   => $biblionumber, ]
1544       [ suspend_until  => $suspend_until, ]
1545       [ suspend        => $suspend ]
1546   );
1547
1548   This function accepts a set of hash keys as its parameters.
1549   It requires either borrowernumber or biblionumber, or both.
1550
1551   suspend_until is wholly optional.
1552
1553 =cut
1554
1555 sub SuspendAll {
1556     my %params = @_;
1557
1558     my $borrowernumber = $params{'borrowernumber'} || undef;
1559     my $biblionumber   = $params{'biblionumber'}   || undef;
1560     my $suspend_until  = $params{'suspend_until'}  || undef;
1561     my $suspend        = defined( $params{'suspend'} ) ? $params{'suspend'} :  1;
1562
1563     $suspend_until = C4::Dates->new( $suspend_until )->output("iso") if ( defined( $suspend_until ) );
1564
1565     return unless ( $borrowernumber || $biblionumber );
1566
1567     my ( $query, $sth, $dbh, @query_params );
1568
1569     $query = "UPDATE reserves SET suspend = ? ";
1570     push( @query_params, $suspend );
1571     if ( !$suspend ) {
1572         $query .= ", suspend_until = NULL ";
1573     } elsif ( $suspend_until ) {
1574         $query .= ", suspend_until = ? ";
1575         push( @query_params, $suspend_until );
1576     }
1577     $query .= " WHERE ";
1578     if ( $borrowernumber ) {
1579         $query .= " borrowernumber = ? ";
1580         push( @query_params, $borrowernumber );
1581     }
1582     $query .= " AND " if ( $borrowernumber && $biblionumber );
1583     if ( $biblionumber ) {
1584         $query .= " biblionumber = ? ";
1585         push( @query_params, $biblionumber );
1586     }
1587     $query .= " AND found IS NULL ";
1588
1589     $dbh = C4::Context->dbh;
1590     $sth = $dbh->prepare( $query );
1591     $sth->execute( @query_params );
1592     $sth->finish;
1593 }
1594
1595
1596 =head2 _FixPriority
1597
1598   &_FixPriority( $reserve_id, $rank, $ignoreSetLowestRank);
1599
1600 Only used internally (so don't export it)
1601 Changed how this functions works #
1602 Now just gets an array of reserves in the rank order and updates them with
1603 the array index (+1 as array starts from 0)
1604 and if $rank is supplied will splice item from the array and splice it back in again
1605 in new priority rank
1606
1607 =cut 
1608
1609 sub _FixPriority {
1610     my ( $reserve_id, $rank, $ignoreSetLowestRank ) = @_;
1611     my $dbh = C4::Context->dbh;
1612
1613     my $res = GetReserve( $reserve_id );
1614
1615     if ( $rank eq "del" ) {
1616          CancelReserve({ reserve_id => $reserve_id });
1617     }
1618     elsif ( $rank eq "W" || $rank eq "0" ) {
1619
1620         # make sure priority for waiting or in-transit items is 0
1621         my $query = "
1622             UPDATE reserves
1623             SET    priority = 0
1624             WHERE reserve_id = ?
1625             AND found IN ('W', 'T')
1626         ";
1627         my $sth = $dbh->prepare($query);
1628         $sth->execute( $reserve_id );
1629     }
1630     my @priority;
1631     my @reservedates;
1632
1633     # get whats left
1634     my $query = "
1635         SELECT reserve_id, borrowernumber, reservedate, constrainttype
1636         FROM   reserves
1637         WHERE  biblionumber   = ?
1638           AND  ((found <> 'W' AND found <> 'T') OR found IS NULL)
1639         ORDER BY priority ASC
1640     ";
1641     my $sth = $dbh->prepare($query);
1642     $sth->execute( $res->{'biblionumber'} );
1643     while ( my $line = $sth->fetchrow_hashref ) {
1644         push( @reservedates, $line );
1645         push( @priority,     $line );
1646     }
1647
1648     # To find the matching index
1649     my $i;
1650     my $key = -1;    # to allow for 0 to be a valid result
1651     for ( $i = 0 ; $i < @priority ; $i++ ) {
1652         if ( $reserve_id == $priority[$i]->{'reserve_id'} ) {
1653             $key = $i;    # save the index
1654             last;
1655         }
1656     }
1657
1658     # if index exists in array then move it to new position
1659     if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1660         my $new_rank = $rank -
1661           1;    # $new_rank is what you want the new index to be in the array
1662         my $moving_item = splice( @priority, $key, 1 );
1663         splice( @priority, $new_rank, 0, $moving_item );
1664     }
1665
1666     # now fix the priority on those that are left....
1667     $query = "
1668         UPDATE reserves
1669         SET    priority = ?
1670         WHERE  reserve_id = ?
1671     ";
1672     $sth = $dbh->prepare($query);
1673     for ( my $j = 0 ; $j < @priority ; $j++ ) {
1674         $sth->execute(
1675             $j + 1,
1676             $priority[$j]->{'reserve_id'}
1677         );
1678         $sth->finish;
1679     }
1680     
1681     $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1682     $sth->execute();
1683     
1684     unless ( $ignoreSetLowestRank ) {
1685       while ( my $res = $sth->fetchrow_hashref() ) {
1686         _FixPriority( $res->{'reserve_id'}, '999999', 1 );
1687       }
1688     }
1689 }
1690
1691 =head2 _Findgroupreserve
1692
1693   @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber);
1694
1695 Looks for an item-specific match first, then for a title-level match, returning the
1696 first match found.  If neither, then we look for a 3rd kind of match based on
1697 reserve constraints.
1698
1699 TODO: add more explanation about reserve constraints
1700
1701 C<&_Findgroupreserve> returns :
1702 C<@results> is an array of references-to-hash whose keys are mostly
1703 fields from the reserves table of the Koha database, plus
1704 C<biblioitemnumber>.
1705
1706 =cut
1707
1708 sub _Findgroupreserve {
1709     my ( $bibitem, $biblio, $itemnumber ) = @_;
1710     my $dbh   = C4::Context->dbh;
1711
1712     # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1713     # check for exact targetted match
1714     my $item_level_target_query = qq/
1715         SELECT reserves.biblionumber        AS biblionumber,
1716                reserves.borrowernumber      AS borrowernumber,
1717                reserves.reservedate         AS reservedate,
1718                reserves.branchcode          AS branchcode,
1719                reserves.cancellationdate    AS cancellationdate,
1720                reserves.found               AS found,
1721                reserves.reservenotes        AS reservenotes,
1722                reserves.priority            AS priority,
1723                reserves.timestamp           AS timestamp,
1724                biblioitems.biblioitemnumber AS biblioitemnumber,
1725                reserves.itemnumber          AS itemnumber,
1726                reserves.reserve_id          AS reserve_id
1727         FROM reserves
1728         JOIN biblioitems USING (biblionumber)
1729         JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1730         WHERE found IS NULL
1731         AND priority > 0
1732         AND item_level_request = 1
1733         AND itemnumber = ?
1734         AND reservedate <= CURRENT_DATE()
1735         AND suspend = 0
1736     /;
1737     my $sth = $dbh->prepare($item_level_target_query);
1738     $sth->execute($itemnumber);
1739     my @results;
1740     if ( my $data = $sth->fetchrow_hashref ) {
1741         push( @results, $data );
1742     }
1743     return @results if @results;
1744     
1745     # check for title-level targetted match
1746     my $title_level_target_query = qq/
1747         SELECT reserves.biblionumber        AS biblionumber,
1748                reserves.borrowernumber      AS borrowernumber,
1749                reserves.reservedate         AS reservedate,
1750                reserves.branchcode          AS branchcode,
1751                reserves.cancellationdate    AS cancellationdate,
1752                reserves.found               AS found,
1753                reserves.reservenotes        AS reservenotes,
1754                reserves.priority            AS priority,
1755                reserves.timestamp           AS timestamp,
1756                biblioitems.biblioitemnumber AS biblioitemnumber,
1757                reserves.itemnumber          AS itemnumber
1758         FROM reserves
1759         JOIN biblioitems USING (biblionumber)
1760         JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1761         WHERE found IS NULL
1762         AND priority > 0
1763         AND item_level_request = 0
1764         AND hold_fill_targets.itemnumber = ?
1765         AND reservedate <= CURRENT_DATE()
1766         AND suspend = 0
1767     /;
1768     $sth = $dbh->prepare($title_level_target_query);
1769     $sth->execute($itemnumber);
1770     @results = ();
1771     if ( my $data = $sth->fetchrow_hashref ) {
1772         push( @results, $data );
1773     }
1774     return @results if @results;
1775
1776     my $query = qq/
1777         SELECT reserves.biblionumber               AS biblionumber,
1778                reserves.borrowernumber             AS borrowernumber,
1779                reserves.reservedate                AS reservedate,
1780                reserves.waitingdate                AS waitingdate,
1781                reserves.branchcode                 AS branchcode,
1782                reserves.cancellationdate           AS cancellationdate,
1783                reserves.found                      AS found,
1784                reserves.reservenotes               AS reservenotes,
1785                reserves.priority                   AS priority,
1786                reserves.timestamp                  AS timestamp,
1787                reserveconstraints.biblioitemnumber AS biblioitemnumber,
1788                reserves.itemnumber                 AS itemnumber
1789         FROM reserves
1790           LEFT JOIN reserveconstraints ON reserves.biblionumber = reserveconstraints.biblionumber
1791         WHERE reserves.biblionumber = ?
1792           AND ( ( reserveconstraints.biblioitemnumber = ?
1793           AND reserves.borrowernumber = reserveconstraints.borrowernumber
1794           AND reserves.reservedate    = reserveconstraints.reservedate )
1795           OR  reserves.constrainttype='a' )
1796           AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1797           AND reserves.reservedate <= CURRENT_DATE()
1798           AND suspend = 0
1799     /;
1800     $sth = $dbh->prepare($query);
1801     $sth->execute( $biblio, $bibitem, $itemnumber );
1802     @results = ();
1803     while ( my $data = $sth->fetchrow_hashref ) {
1804         push( @results, $data );
1805     }
1806     return @results;
1807 }
1808
1809 =head2 _koha_notify_reserve
1810
1811   _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber );
1812
1813 Sends a notification to the patron that their hold has been filled (through
1814 ModReserveAffect, _not_ ModReserveFill)
1815
1816 =cut
1817
1818 sub _koha_notify_reserve {
1819     my ($itemnumber, $borrowernumber, $biblionumber) = @_;
1820
1821     my $dbh = C4::Context->dbh;
1822     my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
1823     
1824     # Try to get the borrower's email address
1825     my $to_address = C4::Members::GetNoticeEmailAddress($borrowernumber);
1826     
1827     my $letter_code;
1828     my $print_mode = 0;
1829     my $messagingprefs;
1830     if ( $to_address || $borrower->{'smsalertnumber'} ) {
1831         $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $borrowernumber, message_name => 'Hold_Filled' } );
1832     } else {
1833         $print_mode = 1;
1834     }
1835
1836     my $sth = $dbh->prepare("
1837         SELECT *
1838         FROM   reserves
1839         WHERE  borrowernumber = ?
1840             AND biblionumber = ?
1841     ");
1842     $sth->execute( $borrowernumber, $biblionumber );
1843     my $reserve = $sth->fetchrow_hashref;
1844     my $branch_details = GetBranchDetail( $reserve->{'branchcode'} );
1845
1846     my $admin_email_address = $branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
1847
1848     my %letter_params = (
1849         module => 'reserves',
1850         branchcode => $reserve->{branchcode},
1851         tables => {
1852             'branches'  => $branch_details,
1853             'borrowers' => $borrower,
1854             'biblio'    => $biblionumber,
1855             'reserves'  => $reserve,
1856             'items', $reserve->{'itemnumber'},
1857         },
1858         substitute => { today => C4::Dates->new()->output() },
1859     );
1860
1861
1862     if ( $print_mode ) {
1863         $letter_params{ 'letter_code' } = 'HOLD_PRINT';
1864         my $letter =  C4::Letters::GetPreparedLetter ( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module";
1865
1866         C4::Letters::EnqueueLetter( {
1867             letter => $letter,
1868             borrowernumber => $borrowernumber,
1869             message_transport_type => 'print',
1870         } );
1871         
1872         return;
1873     }
1874
1875     if ( $to_address && defined $messagingprefs->{transports}->{'email'} ) {
1876         $letter_params{ 'letter_code' } = $messagingprefs->{transports}->{'email'};
1877         my $letter =  C4::Letters::GetPreparedLetter ( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module";
1878
1879         C4::Letters::EnqueueLetter(
1880             {   letter                 => $letter,
1881                 borrowernumber         => $borrowernumber,
1882                 message_transport_type => 'email',
1883                 from_address           => $admin_email_address,
1884             }
1885         );
1886     }
1887
1888     if ( $borrower->{'smsalertnumber'} && defined $messagingprefs->{transports}->{'sms'} ) {
1889         $letter_params{ 'letter_code' } = $messagingprefs->{transports}->{'sms'};
1890         my $letter =  C4::Letters::GetPreparedLetter ( %letter_params ) or die "Could not find a letter called '$letter_params{'letter_code'}' in the 'reserves' module";
1891
1892         C4::Letters::EnqueueLetter(
1893             {   letter                 => $letter,
1894                 borrowernumber         => $borrowernumber,
1895                 message_transport_type => 'sms',
1896             }
1897         );
1898     }
1899 }
1900
1901 =head2 _ShiftPriorityByDateAndPriority
1902
1903   $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1904
1905 This increments the priority of all reserves after the one
1906 with either the lowest date after C<$reservedate>
1907 or the lowest priority after C<$priority>.
1908
1909 It effectively makes room for a new reserve to be inserted with a certain
1910 priority, which is returned.
1911
1912 This is most useful when the reservedate can be set by the user.  It allows
1913 the new reserve to be placed before other reserves that have a later
1914 reservedate.  Since priority also is set by the form in reserves/request.pl
1915 the sub accounts for that too.
1916
1917 =cut
1918
1919 sub _ShiftPriorityByDateAndPriority {
1920     my ( $biblio, $resdate, $new_priority ) = @_;
1921
1922     my $dbh = C4::Context->dbh;
1923     my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1924     my $sth = $dbh->prepare( $query );
1925     $sth->execute( $biblio, $resdate, $new_priority );
1926     my $min_priority = $sth->fetchrow;
1927     # if no such matches are found, $new_priority remains as original value
1928     $new_priority = $min_priority if ( $min_priority );
1929
1930     # Shift the priority up by one; works in conjunction with the next SQL statement
1931     $query = "UPDATE reserves
1932               SET priority = priority+1
1933               WHERE biblionumber = ?
1934               AND borrowernumber = ?
1935               AND reservedate = ?
1936               AND found IS NULL";
1937     my $sth_update = $dbh->prepare( $query );
1938
1939     # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1940     $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1941     $sth = $dbh->prepare( $query );
1942     $sth->execute( $new_priority, $biblio );
1943     while ( my $row = $sth->fetchrow_hashref ) {
1944         $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
1945     }
1946
1947     return $new_priority;  # so the caller knows what priority they wind up receiving
1948 }
1949
1950 =head2 MoveReserve
1951
1952   MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
1953
1954 Use when checking out an item to handle reserves
1955 If $cancelreserve boolean is set to true, it will remove existing reserve
1956
1957 =cut
1958
1959 sub MoveReserve {
1960     my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
1961
1962     my ( $restype, $res, $all_reserves ) = CheckReserves( $itemnumber );
1963     return unless $res;
1964
1965     my $biblionumber     =  $res->{biblionumber};
1966     my $biblioitemnumber = $res->{biblioitemnumber};
1967
1968     if ($res->{borrowernumber} == $borrowernumber) {
1969         ModReserveFill($res);
1970     }
1971     else {
1972         # warn "Reserved";
1973         # The item is reserved by someone else.
1974         # Find this item in the reserves
1975
1976         my $borr_res;
1977         foreach (@$all_reserves) {
1978             $_->{'borrowernumber'} == $borrowernumber or next;
1979             $_->{'biblionumber'}   == $biblionumber   or next;
1980
1981             $borr_res = $_;
1982             last;
1983         }
1984
1985         if ( $borr_res ) {
1986             # The item is reserved by the current patron
1987             ModReserveFill($borr_res);
1988         }
1989
1990         if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
1991             RevertWaitingStatus({ itemnumber => $itemnumber });
1992         }
1993         elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
1994             CancelReserve({
1995                 biblionumber   => $res->{'biblionumber'},
1996                 itemnumber     => $res->{'itemnumber'},
1997                 borrowernumber => $res->{'borrowernumber'}
1998             });
1999         }
2000     }
2001 }
2002
2003 =head2 MergeHolds
2004
2005   MergeHolds($dbh,$to_biblio, $from_biblio);
2006
2007 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
2008
2009 =cut
2010
2011 sub MergeHolds {
2012     my ( $dbh, $to_biblio, $from_biblio ) = @_;
2013     my $sth = $dbh->prepare(
2014         "SELECT count(*) as reserve_id FROM reserves WHERE biblionumber = ?"
2015     );
2016     $sth->execute($from_biblio);
2017     if ( my $data = $sth->fetchrow_hashref() ) {
2018
2019         # holds exist on old record, if not we don't need to do anything
2020         $sth = $dbh->prepare(
2021             "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
2022         $sth->execute( $to_biblio, $from_biblio );
2023
2024         # Reorder by date
2025         # don't reorder those already waiting
2026
2027         $sth = $dbh->prepare(
2028 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
2029         );
2030         my $upd_sth = $dbh->prepare(
2031 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
2032         AND reservedate = ? AND constrainttype = ? AND (itemnumber = ? or itemnumber is NULL) "
2033         );
2034         $sth->execute( $to_biblio, 'W', 'T' );
2035         my $priority = 1;
2036         while ( my $reserve = $sth->fetchrow_hashref() ) {
2037             $upd_sth->execute(
2038                 $priority,                    $to_biblio,
2039                 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
2040                 $reserve->{'constrainttype'}, $reserve->{'itemnumber'}
2041             );
2042             $priority++;
2043         }
2044     }
2045 }
2046
2047 =head2 RevertWaitingStatus
2048
2049   $success = RevertWaitingStatus({ itemnumber => $itemnumber });
2050
2051   Reverts a 'waiting' hold back to a regular hold with a priority of 1.
2052
2053   Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
2054           item level hold, even if it was only a bibliolevel hold to
2055           begin with. This is because we can no longer know if a hold
2056           was item-level or bib-level after a hold has been set to
2057           waiting status.
2058
2059 =cut
2060
2061 sub RevertWaitingStatus {
2062     my ( $params ) = @_;
2063     my $itemnumber = $params->{'itemnumber'};
2064
2065     return unless ( $itemnumber );
2066
2067     my $dbh = C4::Context->dbh;
2068
2069     ## Get the waiting reserve we want to revert
2070     my $query = "
2071         SELECT * FROM reserves
2072         WHERE itemnumber = ?
2073         AND found IS NOT NULL
2074     ";
2075     my $sth = $dbh->prepare( $query );
2076     $sth->execute( $itemnumber );
2077     my $reserve = $sth->fetchrow_hashref();
2078
2079     ## Increment the priority of all other non-waiting
2080     ## reserves for this bib record
2081     $query = "
2082         UPDATE reserves
2083         SET
2084           priority = priority + 1
2085         WHERE
2086           biblionumber =  ?
2087         AND
2088           priority > 0
2089     ";
2090     $sth = $dbh->prepare( $query );
2091     $sth->execute( $reserve->{'biblionumber'} );
2092
2093     ## Fix up the currently waiting reserve
2094     $query = "
2095     UPDATE reserves
2096     SET
2097       priority = 1,
2098       found = NULL,
2099       waitingdate = NULL
2100     WHERE
2101       reserve_id = ?
2102     ";
2103     $sth = $dbh->prepare( $query );
2104     return $sth->execute( $reserve->{'reserve_id'} );
2105 }
2106
2107 =head2 GetReserveId
2108
2109   $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber [, itemnumber => $itemnumber ] });
2110
2111   Returnes the first reserve id that matches the given criteria
2112
2113 =cut
2114
2115 sub GetReserveId {
2116     my ( $params ) = @_;
2117
2118     return unless ( ( $params->{'biblionumber'} || $params->{'itemnumber'} ) && $params->{'borrowernumber'} );
2119
2120     my $dbh = C4::Context->dbh();
2121
2122     my $sql = "SELECT reserve_id FROM reserves WHERE ";
2123
2124     my @params;
2125     my @limits;
2126     foreach my $key ( keys %$params ) {
2127         if ( defined( $params->{$key} ) ) {
2128             push( @limits, "$key = ?" );
2129             push( @params, $params->{$key} );
2130         }
2131     }
2132
2133     $sql .= join( " AND ", @limits );
2134
2135     my $sth = $dbh->prepare( $sql );
2136     $sth->execute( @params );
2137     my $row = $sth->fetchrow_hashref();
2138
2139     return $row->{'reserve_id'};
2140 }
2141
2142 =head2 ReserveSlip
2143
2144   ReserveSlip($branchcode, $borrowernumber, $biblionumber)
2145
2146   Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
2147
2148 =cut
2149
2150 sub ReserveSlip {
2151     my ($branch, $borrowernumber, $biblionumber) = @_;
2152
2153 #   return unless ( C4::Context->boolean_preference('printreserveslips') );
2154
2155     my $reserve = GetReserveInfo($borrowernumber,$biblionumber )
2156       or return;
2157
2158     return  C4::Letters::GetPreparedLetter (
2159         module => 'circulation',
2160         letter_code => 'RESERVESLIP',
2161         branchcode => $branch,
2162         tables => {
2163             'reserves'    => $reserve,
2164             'branches'    => $reserve->{branchcode},
2165             'borrowers'   => $reserve->{borrowernumber},
2166             'biblio'      => $reserve->{biblionumber},
2167             'items'       => $reserve->{itemnumber},
2168         },
2169     );
2170 }
2171
2172 =head1 AUTHOR
2173
2174 Koha Development Team <http://koha-community.org/>
2175
2176 =cut
2177
2178 1;