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