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