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