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