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