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