Bug 11577: OPAC bootstrap theme changes
[koha-ffzg.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
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 =head2 ModReserve
1097
1098   ModReserve({ rank => $rank,
1099                reserve_id => $reserve_id,
1100                branchcode => $branchcode
1101                [, itemnumber => $itemnumber ]
1102                [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
1103               });
1104
1105 Change a hold request's priority or cancel it.
1106
1107 C<$rank> specifies the effect of the change.  If C<$rank>
1108 is 'W' or 'n', nothing happens.  This corresponds to leaving a
1109 request alone when changing its priority in the holds queue
1110 for a bib.
1111
1112 If C<$rank> is 'del', the hold request is cancelled.
1113
1114 If C<$rank> is an integer greater than zero, the priority of
1115 the request is set to that value.  Since priority != 0 means
1116 that the item is not waiting on the hold shelf, setting the 
1117 priority to a non-zero value also sets the request's found
1118 status and waiting date to NULL. 
1119
1120 The optional C<$itemnumber> parameter is used only when
1121 C<$rank> is a non-zero integer; if supplied, the itemnumber 
1122 of the hold request is set accordingly; if omitted, the itemnumber
1123 is cleared.
1124
1125 B<FIXME:> Note that the forgoing can have the effect of causing
1126 item-level hold requests to turn into title-level requests.  This
1127 will be fixed once reserves has separate columns for requested
1128 itemnumber and supplying itemnumber.
1129
1130 =cut
1131
1132 sub ModReserve {
1133     my ( $params ) = @_;
1134
1135     my $rank = $params->{'rank'};
1136     my $reserve_id = $params->{'reserve_id'};
1137     my $branchcode = $params->{'branchcode'};
1138     my $itemnumber = $params->{'itemnumber'};
1139     my $suspend_until = $params->{'suspend_until'};
1140     my $borrowernumber = $params->{'borrowernumber'};
1141     my $biblionumber = $params->{'biblionumber'};
1142
1143     return if $rank eq "W";
1144     return if $rank eq "n";
1145
1146     return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
1147     $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber }) unless ( $reserve_id );
1148
1149     my $dbh = C4::Context->dbh;
1150     if ( $rank eq "del" ) {
1151         CancelReserve({ reserve_id => $reserve_id });
1152     }
1153     elsif ($rank =~ /^\d+/ and $rank > 0) {
1154         my $query = "
1155             UPDATE reserves SET priority = ? ,branchcode = ?, itemnumber = ?, found = NULL, waitingdate = NULL
1156             WHERE reserve_id = ?
1157         ";
1158         my $sth = $dbh->prepare($query);
1159         $sth->execute( $rank, $branchcode, $itemnumber, $reserve_id );
1160
1161         if ( defined( $suspend_until ) ) {
1162             if ( $suspend_until ) {
1163                 $suspend_until = C4::Dates->new( $suspend_until )->output("iso");
1164                 $dbh->do("UPDATE reserves SET suspend = 1, suspend_until = ? WHERE reserve_id = ?", undef, ( $suspend_until, $reserve_id ) );
1165             } else {
1166                 $dbh->do("UPDATE reserves SET suspend_until = NULL WHERE reserve_id = ?", undef, ( $reserve_id ) );
1167             }
1168         }
1169
1170         _FixPriority({ reserve_id => $reserve_id, rank =>$rank });
1171     }
1172 }
1173
1174 =head2 ModReserveFill
1175
1176   &ModReserveFill($reserve);
1177
1178 Fill a reserve. If I understand this correctly, this means that the
1179 reserved book has been found and given to the patron who reserved it.
1180
1181 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
1182 whose keys are fields from the reserves table in the Koha database.
1183
1184 =cut
1185
1186 sub ModReserveFill {
1187     my ($res) = @_;
1188     my $dbh = C4::Context->dbh;
1189     # fill in a reserve record....
1190     my $reserve_id = $res->{'reserve_id'};
1191     my $biblionumber = $res->{'biblionumber'};
1192     my $borrowernumber    = $res->{'borrowernumber'};
1193     my $resdate = $res->{'reservedate'};
1194
1195     # get the priority on this record....
1196     my $priority;
1197     my $query = "SELECT priority
1198                  FROM   reserves
1199                  WHERE  biblionumber   = ?
1200                   AND   borrowernumber = ?
1201                   AND   reservedate    = ?";
1202     my $sth = $dbh->prepare($query);
1203     $sth->execute( $biblionumber, $borrowernumber, $resdate );
1204     ($priority) = $sth->fetchrow_array;
1205
1206     # update the database...
1207     $query = "UPDATE reserves
1208                   SET    found            = 'F',
1209                          priority         = 0
1210                  WHERE  biblionumber     = ?
1211                     AND reservedate      = ?
1212                     AND borrowernumber   = ?
1213                 ";
1214     $sth = $dbh->prepare($query);
1215     $sth->execute( $biblionumber, $resdate, $borrowernumber );
1216
1217     # move to old_reserves
1218     $query = "INSERT INTO old_reserves
1219                  SELECT * FROM reserves
1220                  WHERE  biblionumber     = ?
1221                     AND reservedate      = ?
1222                     AND borrowernumber   = ?
1223                 ";
1224     $sth = $dbh->prepare($query);
1225     $sth->execute( $biblionumber, $resdate, $borrowernumber );
1226     $query = "DELETE FROM reserves
1227                  WHERE  biblionumber     = ?
1228                     AND reservedate      = ?
1229                     AND borrowernumber   = ?
1230                 ";
1231     $sth = $dbh->prepare($query);
1232     $sth->execute( $biblionumber, $resdate, $borrowernumber );
1233     
1234     # now fix the priority on the others (if the priority wasn't
1235     # already sorted!)....
1236     unless ( $priority == 0 ) {
1237         _FixPriority({ reserve_id => $reserve_id });
1238     }
1239 }
1240
1241 =head2 ModReserveStatus
1242
1243   &ModReserveStatus($itemnumber, $newstatus);
1244
1245 Update the reserve status for the active (priority=0) reserve.
1246
1247 $itemnumber is the itemnumber the reserve is on
1248
1249 $newstatus is the new status.
1250
1251 =cut
1252
1253 sub ModReserveStatus {
1254
1255     #first : check if we have a reservation for this item .
1256     my ($itemnumber, $newstatus) = @_;
1257     my $dbh = C4::Context->dbh;
1258
1259     my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1260     my $sth_set = $dbh->prepare($query);
1261     $sth_set->execute( $newstatus, $itemnumber );
1262
1263     if ( C4::Context->preference("ReturnToShelvingCart") && $newstatus ) {
1264       CartToShelf( $itemnumber );
1265     }
1266 }
1267
1268 =head2 ModReserveAffect
1269
1270   &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend);
1271
1272 This function affect an item and a status for a given reserve
1273 The itemnumber parameter is used to find the biblionumber.
1274 with the biblionumber & the borrowernumber, we can affect the itemnumber
1275 to the correct reserve.
1276
1277 if $transferToDo is not set, then the status is set to "Waiting" as well.
1278 otherwise, a transfer is on the way, and the end of the transfer will 
1279 take care of the waiting status
1280
1281 =cut
1282
1283 sub ModReserveAffect {
1284     my ( $itemnumber, $borrowernumber,$transferToDo ) = @_;
1285     my $dbh = C4::Context->dbh;
1286
1287     # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1288     # attached to $itemnumber
1289     my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1290     $sth->execute($itemnumber);
1291     my ($biblionumber) = $sth->fetchrow;
1292
1293     # get request - need to find out if item is already
1294     # waiting in order to not send duplicate hold filled notifications
1295     my $reserve_id = GetReserveId({
1296         borrowernumber => $borrowernumber,
1297         biblionumber   => $biblionumber,
1298     });
1299     return unless defined $reserve_id;
1300     my $request = GetReserveInfo($reserve_id);
1301     my $already_on_shelf = ($request && $request->{found} eq 'W') ? 1 : 0;
1302
1303     # If we affect a reserve that has to be transfered, don't set to Waiting
1304     my $query;
1305     if ($transferToDo) {
1306     $query = "
1307         UPDATE reserves
1308         SET    priority = 0,
1309                itemnumber = ?,
1310                found = 'T'
1311         WHERE borrowernumber = ?
1312           AND biblionumber = ?
1313     ";
1314     }
1315     else {
1316     # affect the reserve to Waiting as well.
1317         $query = "
1318             UPDATE reserves
1319             SET     priority = 0,
1320                     found = 'W',
1321                     waitingdate = NOW(),
1322                     itemnumber = ?
1323             WHERE borrowernumber = ?
1324               AND biblionumber = ?
1325         ";
1326     }
1327     $sth = $dbh->prepare($query);
1328     $sth->execute( $itemnumber, $borrowernumber,$biblionumber);
1329     _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber ) if ( !$transferToDo && !$already_on_shelf );
1330     _FixPriority( { biblionumber => $biblionumber } );
1331     if ( C4::Context->preference("ReturnToShelvingCart") ) {
1332       CartToShelf( $itemnumber );
1333     }
1334
1335     return;
1336 }
1337
1338 =head2 ModReserveCancelAll
1339
1340   ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1341
1342 function to cancel reserv,check other reserves, and transfer document if it's necessary
1343
1344 =cut
1345
1346 sub ModReserveCancelAll {
1347     my $messages;
1348     my $nextreservinfo;
1349     my ( $itemnumber, $borrowernumber ) = @_;
1350
1351     #step 1 : cancel the reservation
1352     my $CancelReserve = CancelReserve({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1353
1354     #step 2 launch the subroutine of the others reserves
1355     ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1356
1357     return ( $messages, $nextreservinfo );
1358 }
1359
1360 =head2 ModReserveMinusPriority
1361
1362   &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1363
1364 Reduce the values of queued list
1365
1366 =cut
1367
1368 sub ModReserveMinusPriority {
1369     my ( $itemnumber, $reserve_id ) = @_;
1370
1371     #first step update the value of the first person on reserv
1372     my $dbh   = C4::Context->dbh;
1373     my $query = "
1374         UPDATE reserves
1375         SET    priority = 0 , itemnumber = ? 
1376         WHERE  reserve_id = ?
1377     ";
1378     my $sth_upd = $dbh->prepare($query);
1379     $sth_upd->execute( $itemnumber, $reserve_id );
1380     # second step update all others reserves
1381     _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1382 }
1383
1384 =head2 GetReserveInfo
1385
1386   &GetReserveInfo($reserve_id);
1387
1388 Get item and borrower details for a current hold.
1389 Current implementation this query should have a single result.
1390
1391 =cut
1392
1393 sub GetReserveInfo {
1394     my ( $reserve_id ) = @_;
1395     my $dbh = C4::Context->dbh;
1396     my $strsth="SELECT
1397                    reserve_id,
1398                    reservedate,
1399                    reservenotes,
1400                    reserves.borrowernumber,
1401                    reserves.biblionumber,
1402                    reserves.branchcode,
1403                    reserves.waitingdate,
1404                    notificationdate,
1405                    reminderdate,
1406                    priority,
1407                    found,
1408                    firstname,
1409                    surname,
1410                    phone,
1411                    email,
1412                    address,
1413                    address2,
1414                    cardnumber,
1415                    city,
1416                    zipcode,
1417                    biblio.title,
1418                    biblio.author,
1419                    items.holdingbranch,
1420                    items.itemcallnumber,
1421                    items.itemnumber,
1422                    items.location,
1423                    barcode,
1424                    notes
1425                 FROM reserves
1426                 LEFT JOIN items USING(itemnumber)
1427                 LEFT JOIN borrowers USING(borrowernumber)
1428                 LEFT JOIN biblio ON  (reserves.biblionumber=biblio.biblionumber)
1429                 WHERE reserves.reserve_id = ?";
1430     my $sth = $dbh->prepare($strsth);
1431     $sth->execute($reserve_id);
1432
1433     my $data = $sth->fetchrow_hashref;
1434     return $data;
1435 }
1436
1437 =head2 IsAvailableForItemLevelRequest
1438
1439   my $is_available = IsAvailableForItemLevelRequest($itemnumber);
1440
1441 Checks whether a given item record is available for an
1442 item-level hold request.  An item is available if
1443
1444 * it is not lost AND 
1445 * it is not damaged AND 
1446 * it is not withdrawn AND 
1447 * does not have a not for loan value > 0
1448
1449 Whether or not the item is currently on loan is 
1450 also checked - if the AllowOnShelfHolds system preference
1451 is ON, an item can be requested even if it is currently
1452 on loan to somebody else.  If the system preference
1453 is OFF, an item that is currently checked out cannot
1454 be the target of an item-level hold request.
1455
1456 Note that IsAvailableForItemLevelRequest() does not
1457 check if the staff operator is authorized to place
1458 a request on the item - in particular,
1459 this routine does not check IndependentBranches
1460 and canreservefromotherbranches.
1461
1462 =cut
1463
1464 sub IsAvailableForItemLevelRequest {
1465     my $itemnumber = shift;
1466    
1467     my $item = GetItem($itemnumber);
1468
1469     # must check the notforloan setting of the itemtype
1470     # FIXME - a lot of places in the code do this
1471     #         or something similar - need to be
1472     #         consolidated
1473     my $dbh = C4::Context->dbh;
1474     my $notforloan_query;
1475     if (C4::Context->preference('item-level_itypes')) {
1476         $notforloan_query = "SELECT itemtypes.notforloan
1477                              FROM items
1478                              JOIN itemtypes ON (itemtypes.itemtype = items.itype)
1479                              WHERE itemnumber = ?";
1480     } else {
1481         $notforloan_query = "SELECT itemtypes.notforloan
1482                              FROM items
1483                              JOIN biblioitems USING (biblioitemnumber)
1484                              JOIN itemtypes USING (itemtype)
1485                              WHERE itemnumber = ?";
1486     }
1487     my $sth = $dbh->prepare($notforloan_query);
1488     $sth->execute($itemnumber);
1489     my $notforloan_per_itemtype = 0;
1490     if (my ($notforloan) = $sth->fetchrow_array) {
1491         $notforloan_per_itemtype = 1 if $notforloan;
1492     }
1493
1494     my $available_per_item = 1;
1495     $available_per_item = 0 if $item->{itemlost} or
1496                                ( $item->{notforloan} > 0 ) or
1497                                ($item->{damaged} and not C4::Context->preference('AllowHoldsOnDamagedItems')) or
1498                                $item->{withdrawn} or
1499                                $notforloan_per_itemtype;
1500
1501
1502     if (C4::Context->preference('AllowOnShelfHolds')) {
1503         return $available_per_item;
1504     } else {
1505         return ($available_per_item and ($item->{onloan} or GetReserveStatus($itemnumber) eq "Waiting"));
1506     }
1507 }
1508
1509 =head2 AlterPriority
1510
1511   AlterPriority( $where, $reserve_id );
1512
1513 This function changes a reserve's priority up, down, to the top, or to the bottom.
1514 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1515
1516 =cut
1517
1518 sub AlterPriority {
1519     my ( $where, $reserve_id ) = @_;
1520
1521     my $dbh = C4::Context->dbh;
1522
1523     my $reserve = GetReserve( $reserve_id );
1524
1525     if ( $reserve->{cancellationdate} ) {
1526         warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (".$reserve->{cancellationdate}.')';
1527         return;
1528     }
1529
1530     if ( $where eq 'up' || $where eq 'down' ) {
1531
1532       my $priority = $reserve->{'priority'};
1533       $priority = $where eq 'up' ? $priority - 1 : $priority + 1;
1534       _FixPriority({ reserve_id => $reserve_id, rank => $priority })
1535
1536     } elsif ( $where eq 'top' ) {
1537
1538       _FixPriority({ reserve_id => $reserve_id, rank => '1' })
1539
1540     } elsif ( $where eq 'bottom' ) {
1541
1542       _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1543
1544     }
1545 }
1546
1547 =head2 ToggleLowestPriority
1548
1549   ToggleLowestPriority( $borrowernumber, $biblionumber );
1550
1551 This function sets the lowestPriority field to true if is false, and false if it is true.
1552
1553 =cut
1554
1555 sub ToggleLowestPriority {
1556     my ( $reserve_id ) = @_;
1557
1558     my $dbh = C4::Context->dbh;
1559
1560     my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1561     $sth->execute( $reserve_id );
1562     
1563     _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1564 }
1565
1566 =head2 ToggleSuspend
1567
1568   ToggleSuspend( $reserve_id );
1569
1570 This function sets the suspend field to true if is false, and false if it is true.
1571 If the reserve is currently suspended with a suspend_until date, that date will
1572 be cleared when it is unsuspended.
1573
1574 =cut
1575
1576 sub ToggleSuspend {
1577     my ( $reserve_id, $suspend_until ) = @_;
1578
1579     $suspend_until = output_pref(
1580         {
1581             dt         => dt_from_string($suspend_until),
1582             dateformat => 'iso',
1583             dateonly   => 1
1584         }
1585     ) if ($suspend_until);
1586
1587     my $do_until = ( $suspend_until ) ? '?' : 'NULL';
1588
1589     my $dbh = C4::Context->dbh;
1590
1591     my $sth = $dbh->prepare(
1592         "UPDATE reserves SET suspend = NOT suspend,
1593         suspend_until = CASE WHEN suspend = 0 THEN NULL ELSE $do_until END
1594         WHERE reserve_id = ?
1595     ");
1596
1597     my @params;
1598     push( @params, $suspend_until ) if ( $suspend_until );
1599     push( @params, $reserve_id );
1600
1601     $sth->execute( @params );
1602 }
1603
1604 =head2 SuspendAll
1605
1606   SuspendAll(
1607       borrowernumber   => $borrowernumber,
1608       [ biblionumber   => $biblionumber, ]
1609       [ suspend_until  => $suspend_until, ]
1610       [ suspend        => $suspend ]
1611   );
1612
1613   This function accepts a set of hash keys as its parameters.
1614   It requires either borrowernumber or biblionumber, or both.
1615
1616   suspend_until is wholly optional.
1617
1618 =cut
1619
1620 sub SuspendAll {
1621     my %params = @_;
1622
1623     my $borrowernumber = $params{'borrowernumber'} || undef;
1624     my $biblionumber   = $params{'biblionumber'}   || undef;
1625     my $suspend_until  = $params{'suspend_until'}  || undef;
1626     my $suspend        = defined( $params{'suspend'} ) ? $params{'suspend'} :  1;
1627
1628     $suspend_until = C4::Dates->new( $suspend_until )->output("iso") if ( defined( $suspend_until ) );
1629
1630     return unless ( $borrowernumber || $biblionumber );
1631
1632     my ( $query, $sth, $dbh, @query_params );
1633
1634     $query = "UPDATE reserves SET suspend = ? ";
1635     push( @query_params, $suspend );
1636     if ( !$suspend ) {
1637         $query .= ", suspend_until = NULL ";
1638     } elsif ( $suspend_until ) {
1639         $query .= ", suspend_until = ? ";
1640         push( @query_params, $suspend_until );
1641     }
1642     $query .= " WHERE ";
1643     if ( $borrowernumber ) {
1644         $query .= " borrowernumber = ? ";
1645         push( @query_params, $borrowernumber );
1646     }
1647     $query .= " AND " if ( $borrowernumber && $biblionumber );
1648     if ( $biblionumber ) {
1649         $query .= " biblionumber = ? ";
1650         push( @query_params, $biblionumber );
1651     }
1652     $query .= " AND found IS NULL ";
1653
1654     $dbh = C4::Context->dbh;
1655     $sth = $dbh->prepare( $query );
1656     $sth->execute( @query_params );
1657 }
1658
1659
1660 =head2 _FixPriority
1661
1662   _FixPriority({
1663     reserve_id => $reserve_id,
1664     [rank => $rank,]
1665     [ignoreSetLowestRank => $ignoreSetLowestRank]
1666   });
1667
1668   or
1669
1670   _FixPriority({ biblionumber => $biblionumber});
1671
1672 This routine adjusts the priority of a hold request and holds
1673 on the same bib.
1674
1675 In the first form, where a reserve_id is passed, the priority of the
1676 hold is set to supplied rank, and other holds for that bib are adjusted
1677 accordingly.  If the rank is "del", the hold is cancelled.  If no rank
1678 is supplied, all of the holds on that bib have their priority adjusted
1679 as if the second form had been used.
1680
1681 In the second form, where a biblionumber is passed, the holds on that
1682 bib (that are not captured) are sorted in order of increasing priority,
1683 then have reserves.priority set so that the first non-captured hold
1684 has its priority set to 1, the second non-captured hold has its priority
1685 set to 2, and so forth.
1686
1687 In both cases, holds that have the lowestPriority flag on are have their
1688 priority adjusted to ensure that they remain at the end of the line.
1689
1690 Note that the ignoreSetLowestRank parameter is meant to be used only
1691 when _FixPriority calls itself.
1692
1693 =cut
1694
1695 sub _FixPriority {
1696     my ( $params ) = @_;
1697     my $reserve_id = $params->{reserve_id};
1698     my $rank = $params->{rank} // '';
1699     my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1700     my $biblionumber = $params->{biblionumber};
1701
1702     my $dbh = C4::Context->dbh;
1703
1704     unless ( $biblionumber ) {
1705         my $res = GetReserve( $reserve_id );
1706         $biblionumber = $res->{biblionumber};
1707     }
1708
1709     if ( $rank eq "del" ) {
1710          CancelReserve({ reserve_id => $reserve_id });
1711     }
1712     elsif ( $rank eq "W" || $rank eq "0" ) {
1713
1714         # make sure priority for waiting or in-transit items is 0
1715         my $query = "
1716             UPDATE reserves
1717             SET    priority = 0
1718             WHERE reserve_id = ?
1719             AND found IN ('W', 'T')
1720         ";
1721         my $sth = $dbh->prepare($query);
1722         $sth->execute( $reserve_id );
1723     }
1724     my @priority;
1725
1726     # get whats left
1727     my $query = "
1728         SELECT reserve_id, borrowernumber, reservedate, constrainttype
1729         FROM   reserves
1730         WHERE  biblionumber   = ?
1731           AND  ((found <> 'W' AND found <> 'T') OR found IS NULL)
1732         ORDER BY priority ASC
1733     ";
1734     my $sth = $dbh->prepare($query);
1735     $sth->execute( $biblionumber );
1736     while ( my $line = $sth->fetchrow_hashref ) {
1737         push( @priority,     $line );
1738     }
1739
1740     # To find the matching index
1741     my $i;
1742     my $key = -1;    # to allow for 0 to be a valid result
1743     for ( $i = 0 ; $i < @priority ; $i++ ) {
1744         if ( $reserve_id == $priority[$i]->{'reserve_id'} ) {
1745             $key = $i;    # save the index
1746             last;
1747         }
1748     }
1749
1750     # if index exists in array then move it to new position
1751     if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1752         my $new_rank = $rank -
1753           1;    # $new_rank is what you want the new index to be in the array
1754         my $moving_item = splice( @priority, $key, 1 );
1755         splice( @priority, $new_rank, 0, $moving_item );
1756     }
1757
1758     # now fix the priority on those that are left....
1759     $query = "
1760         UPDATE reserves
1761         SET    priority = ?
1762         WHERE  reserve_id = ?
1763     ";
1764     $sth = $dbh->prepare($query);
1765     for ( my $j = 0 ; $j < @priority ; $j++ ) {
1766         $sth->execute(
1767             $j + 1,
1768             $priority[$j]->{'reserve_id'}
1769         );
1770     }
1771     
1772     $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1773     $sth->execute();
1774     
1775     unless ( $ignoreSetLowestRank ) {
1776       while ( my $res = $sth->fetchrow_hashref() ) {
1777         _FixPriority({
1778             reserve_id => $res->{'reserve_id'},
1779             rank => '999999',
1780             ignoreSetLowestRank => 1
1781         });
1782       }
1783     }
1784 }
1785
1786 =head2 _Findgroupreserve
1787
1788   @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead);
1789
1790 Looks for an item-specific match first, then for a title-level match, returning the
1791 first match found.  If neither, then we look for a 3rd kind of match based on
1792 reserve constraints.
1793 Lookahead is the number of days to look in advance.
1794
1795 TODO: add more explanation about reserve constraints
1796
1797 C<&_Findgroupreserve> returns :
1798 C<@results> is an array of references-to-hash whose keys are mostly
1799 fields from the reserves table of the Koha database, plus
1800 C<biblioitemnumber>.
1801
1802 =cut
1803
1804 sub _Findgroupreserve {
1805     my ( $bibitem, $biblio, $itemnumber, $lookahead) = @_;
1806     my $dbh   = C4::Context->dbh;
1807
1808     # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1809     # check for exact targetted match
1810     my $item_level_target_query = qq/
1811         SELECT reserves.biblionumber        AS biblionumber,
1812                reserves.borrowernumber      AS borrowernumber,
1813                reserves.reservedate         AS reservedate,
1814                reserves.branchcode          AS branchcode,
1815                reserves.cancellationdate    AS cancellationdate,
1816                reserves.found               AS found,
1817                reserves.reservenotes        AS reservenotes,
1818                reserves.priority            AS priority,
1819                reserves.timestamp           AS timestamp,
1820                biblioitems.biblioitemnumber AS biblioitemnumber,
1821                reserves.itemnumber          AS itemnumber,
1822                reserves.reserve_id          AS reserve_id
1823         FROM reserves
1824         JOIN biblioitems USING (biblionumber)
1825         JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1826         WHERE found IS NULL
1827         AND priority > 0
1828         AND item_level_request = 1
1829         AND itemnumber = ?
1830         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1831         AND suspend = 0
1832     /;
1833     my $sth = $dbh->prepare($item_level_target_query);
1834     $sth->execute($itemnumber, $lookahead||0);
1835     my @results;
1836     if ( my $data = $sth->fetchrow_hashref ) {
1837         push( @results, $data );
1838     }
1839     return @results if @results;
1840     
1841     # check for title-level targetted match
1842     my $title_level_target_query = qq/
1843         SELECT reserves.biblionumber        AS biblionumber,
1844                reserves.borrowernumber      AS borrowernumber,
1845                reserves.reservedate         AS reservedate,
1846                reserves.branchcode          AS branchcode,
1847                reserves.cancellationdate    AS cancellationdate,
1848                reserves.found               AS found,
1849                reserves.reservenotes        AS reservenotes,
1850                reserves.priority            AS priority,
1851                reserves.timestamp           AS timestamp,
1852                biblioitems.biblioitemnumber AS biblioitemnumber,
1853                reserves.itemnumber          AS itemnumber,
1854                reserves.reserve_id          AS reserve_id
1855         FROM reserves
1856         JOIN biblioitems USING (biblionumber)
1857         JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1858         WHERE found IS NULL
1859         AND priority > 0
1860         AND item_level_request = 0
1861         AND hold_fill_targets.itemnumber = ?
1862         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1863         AND suspend = 0
1864     /;
1865     $sth = $dbh->prepare($title_level_target_query);
1866     $sth->execute($itemnumber, $lookahead||0);
1867     @results = ();
1868     if ( my $data = $sth->fetchrow_hashref ) {
1869         push( @results, $data );
1870     }
1871     return @results if @results;
1872
1873     my $query = qq/
1874         SELECT reserves.biblionumber               AS biblionumber,
1875                reserves.borrowernumber             AS borrowernumber,
1876                reserves.reservedate                AS reservedate,
1877                reserves.waitingdate                AS waitingdate,
1878                reserves.branchcode                 AS branchcode,
1879                reserves.cancellationdate           AS cancellationdate,
1880                reserves.found                      AS found,
1881                reserves.reservenotes               AS reservenotes,
1882                reserves.priority                   AS priority,
1883                reserves.timestamp                  AS timestamp,
1884                reserveconstraints.biblioitemnumber AS biblioitemnumber,
1885                reserves.itemnumber                 AS itemnumber,
1886                reserves.reserve_id                 AS reserve_id
1887         FROM reserves
1888           LEFT JOIN reserveconstraints ON reserves.biblionumber = reserveconstraints.biblionumber
1889         WHERE reserves.biblionumber = ?
1890           AND ( ( reserveconstraints.biblioitemnumber = ?
1891           AND reserves.borrowernumber = reserveconstraints.borrowernumber
1892           AND reserves.reservedate    = reserveconstraints.reservedate )
1893           OR  reserves.constrainttype='a' )
1894           AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1895           AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1896           AND suspend = 0
1897     /;
1898     $sth = $dbh->prepare($query);
1899     $sth->execute( $biblio, $bibitem, $itemnumber, $lookahead||0);
1900     @results = ();
1901     while ( my $data = $sth->fetchrow_hashref ) {
1902         push( @results, $data );
1903     }
1904     return @results;
1905 }
1906
1907 =head2 _koha_notify_reserve
1908
1909   _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber );
1910
1911 Sends a notification to the patron that their hold has been filled (through
1912 ModReserveAffect, _not_ ModReserveFill)
1913
1914 =cut
1915
1916 sub _koha_notify_reserve {
1917     my ($itemnumber, $borrowernumber, $biblionumber) = @_;
1918
1919     my $dbh = C4::Context->dbh;
1920     my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
1921     
1922     # Try to get the borrower's email address
1923     my $to_address = C4::Members::GetNoticeEmailAddress($borrowernumber);
1924
1925     my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
1926             borrowernumber => $borrowernumber,
1927             message_name => 'Hold_Filled'
1928     } );
1929
1930     my $sth = $dbh->prepare("
1931         SELECT *
1932         FROM   reserves
1933         WHERE  borrowernumber = ?
1934             AND biblionumber = ?
1935     ");
1936     $sth->execute( $borrowernumber, $biblionumber );
1937     my $reserve = $sth->fetchrow_hashref;
1938     my $branch_details = GetBranchDetail( $reserve->{'branchcode'} );
1939
1940     my $admin_email_address = $branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
1941
1942     my %letter_params = (
1943         module => 'reserves',
1944         branchcode => $reserve->{branchcode},
1945         tables => {
1946             'branches'  => $branch_details,
1947             'borrowers' => $borrower,
1948             'biblio'    => $biblionumber,
1949             'reserves'  => $reserve,
1950             'items', $reserve->{'itemnumber'},
1951         },
1952         substitute => { today => C4::Dates->new()->output() },
1953     );
1954
1955     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.
1956     my $send_notification = sub {
1957         my ( $mtt, $letter_code ) = (@_);
1958         return unless defined $letter_code;
1959         $letter_params{letter_code} = $letter_code;
1960         $letter_params{message_transport_type} = $mtt;
1961         my $letter =  C4::Letters::GetPreparedLetter ( %letter_params );
1962         unless ($letter) {
1963             warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
1964             return;
1965         }
1966
1967         C4::Letters::EnqueueLetter( {
1968             letter => $letter,
1969             borrowernumber => $borrowernumber,
1970             from_address => $admin_email_address,
1971             message_transport_type => $mtt,
1972         } );
1973     };
1974
1975     while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
1976         if ( ($mtt eq 'email' and not $to_address) or ($mtt eq 'sms' and not $borrower->{smsalertnumber}) ) {
1977             # email or sms is requested but not exist
1978             next;
1979         }
1980         &$send_notification($mtt, $letter_code);
1981         $notification_sent++;
1982     }
1983     #Making sure that a print notification is sent if no other transport types can be utilized.
1984     if (! $notification_sent) {
1985         &$send_notification('print', 'HOLD');
1986     }
1987     
1988 }
1989
1990 =head2 _ShiftPriorityByDateAndPriority
1991
1992   $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1993
1994 This increments the priority of all reserves after the one
1995 with either the lowest date after C<$reservedate>
1996 or the lowest priority after C<$priority>.
1997
1998 It effectively makes room for a new reserve to be inserted with a certain
1999 priority, which is returned.
2000
2001 This is most useful when the reservedate can be set by the user.  It allows
2002 the new reserve to be placed before other reserves that have a later
2003 reservedate.  Since priority also is set by the form in reserves/request.pl
2004 the sub accounts for that too.
2005
2006 =cut
2007
2008 sub _ShiftPriorityByDateAndPriority {
2009     my ( $biblio, $resdate, $new_priority ) = @_;
2010
2011     my $dbh = C4::Context->dbh;
2012     my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
2013     my $sth = $dbh->prepare( $query );
2014     $sth->execute( $biblio, $resdate, $new_priority );
2015     my $min_priority = $sth->fetchrow;
2016     # if no such matches are found, $new_priority remains as original value
2017     $new_priority = $min_priority if ( $min_priority );
2018
2019     # Shift the priority up by one; works in conjunction with the next SQL statement
2020     $query = "UPDATE reserves
2021               SET priority = priority+1
2022               WHERE biblionumber = ?
2023               AND borrowernumber = ?
2024               AND reservedate = ?
2025               AND found IS NULL";
2026     my $sth_update = $dbh->prepare( $query );
2027
2028     # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
2029     $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
2030     $sth = $dbh->prepare( $query );
2031     $sth->execute( $new_priority, $biblio );
2032     while ( my $row = $sth->fetchrow_hashref ) {
2033         $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
2034     }
2035
2036     return $new_priority;  # so the caller knows what priority they wind up receiving
2037 }
2038
2039 =head2 MoveReserve
2040
2041   MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
2042
2043 Use when checking out an item to handle reserves
2044 If $cancelreserve boolean is set to true, it will remove existing reserve
2045
2046 =cut
2047
2048 sub MoveReserve {
2049     my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
2050
2051     my ( $restype, $res, $all_reserves ) = CheckReserves( $itemnumber );
2052     return unless $res;
2053
2054     my $biblionumber     =  $res->{biblionumber};
2055     my $biblioitemnumber = $res->{biblioitemnumber};
2056
2057     if ($res->{borrowernumber} == $borrowernumber) {
2058         ModReserveFill($res);
2059     }
2060     else {
2061         # warn "Reserved";
2062         # The item is reserved by someone else.
2063         # Find this item in the reserves
2064
2065         my $borr_res;
2066         foreach (@$all_reserves) {
2067             $_->{'borrowernumber'} == $borrowernumber or next;
2068             $_->{'biblionumber'}   == $biblionumber   or next;
2069
2070             $borr_res = $_;
2071             last;
2072         }
2073
2074         if ( $borr_res ) {
2075             # The item is reserved by the current patron
2076             ModReserveFill($borr_res);
2077         }
2078
2079         if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
2080             RevertWaitingStatus({ itemnumber => $itemnumber });
2081         }
2082         elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
2083             CancelReserve({
2084                 biblionumber   => $res->{'biblionumber'},
2085                 itemnumber     => $res->{'itemnumber'},
2086                 borrowernumber => $res->{'borrowernumber'}
2087             });
2088         }
2089     }
2090 }
2091
2092 =head2 MergeHolds
2093
2094   MergeHolds($dbh,$to_biblio, $from_biblio);
2095
2096 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
2097
2098 =cut
2099
2100 sub MergeHolds {
2101     my ( $dbh, $to_biblio, $from_biblio ) = @_;
2102     my $sth = $dbh->prepare(
2103         "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
2104     );
2105     $sth->execute($from_biblio);
2106     if ( my $data = $sth->fetchrow_hashref() ) {
2107
2108         # holds exist on old record, if not we don't need to do anything
2109         $sth = $dbh->prepare(
2110             "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
2111         $sth->execute( $to_biblio, $from_biblio );
2112
2113         # Reorder by date
2114         # don't reorder those already waiting
2115
2116         $sth = $dbh->prepare(
2117 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
2118         );
2119         my $upd_sth = $dbh->prepare(
2120 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
2121         AND reservedate = ? AND constrainttype = ? AND (itemnumber = ? or itemnumber is NULL) "
2122         );
2123         $sth->execute( $to_biblio, 'W', 'T' );
2124         my $priority = 1;
2125         while ( my $reserve = $sth->fetchrow_hashref() ) {
2126             $upd_sth->execute(
2127                 $priority,                    $to_biblio,
2128                 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
2129                 $reserve->{'constrainttype'}, $reserve->{'itemnumber'}
2130             );
2131             $priority++;
2132         }
2133     }
2134 }
2135
2136 =head2 RevertWaitingStatus
2137
2138   RevertWaitingStatus({ itemnumber => $itemnumber });
2139
2140   Reverts a 'waiting' hold back to a regular hold with a priority of 1.
2141
2142   Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
2143           item level hold, even if it was only a bibliolevel hold to
2144           begin with. This is because we can no longer know if a hold
2145           was item-level or bib-level after a hold has been set to
2146           waiting status.
2147
2148 =cut
2149
2150 sub RevertWaitingStatus {
2151     my ( $params ) = @_;
2152     my $itemnumber = $params->{'itemnumber'};
2153
2154     return unless ( $itemnumber );
2155
2156     my $dbh = C4::Context->dbh;
2157
2158     ## Get the waiting reserve we want to revert
2159     my $query = "
2160         SELECT * FROM reserves
2161         WHERE itemnumber = ?
2162         AND found IS NOT NULL
2163     ";
2164     my $sth = $dbh->prepare( $query );
2165     $sth->execute( $itemnumber );
2166     my $reserve = $sth->fetchrow_hashref();
2167
2168     ## Increment the priority of all other non-waiting
2169     ## reserves for this bib record
2170     $query = "
2171         UPDATE reserves
2172         SET
2173           priority = priority + 1
2174         WHERE
2175           biblionumber =  ?
2176         AND
2177           priority > 0
2178     ";
2179     $sth = $dbh->prepare( $query );
2180     $sth->execute( $reserve->{'biblionumber'} );
2181
2182     ## Fix up the currently waiting reserve
2183     $query = "
2184     UPDATE reserves
2185     SET
2186       priority = 1,
2187       found = NULL,
2188       waitingdate = NULL
2189     WHERE
2190       reserve_id = ?
2191     ";
2192     $sth = $dbh->prepare( $query );
2193     $sth->execute( $reserve->{'reserve_id'} );
2194     _FixPriority( { biblionumber => $reserve->{biblionumber} } );
2195 }
2196
2197 =head2 GetReserveId
2198
2199   $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber [, itemnumber => $itemnumber ] });
2200
2201   Returnes the first reserve id that matches the given criteria
2202
2203 =cut
2204
2205 sub GetReserveId {
2206     my ( $params ) = @_;
2207
2208     return unless ( ( $params->{'biblionumber'} || $params->{'itemnumber'} ) && $params->{'borrowernumber'} );
2209
2210     my $dbh = C4::Context->dbh();
2211
2212     my $sql = "SELECT reserve_id FROM reserves WHERE ";
2213
2214     my @params;
2215     my @limits;
2216     foreach my $key ( keys %$params ) {
2217         if ( defined( $params->{$key} ) ) {
2218             push( @limits, "$key = ?" );
2219             push( @params, $params->{$key} );
2220         }
2221     }
2222
2223     $sql .= join( " AND ", @limits );
2224
2225     my $sth = $dbh->prepare( $sql );
2226     $sth->execute( @params );
2227     my $row = $sth->fetchrow_hashref();
2228
2229     return $row->{'reserve_id'};
2230 }
2231
2232 =head2 ReserveSlip
2233
2234   ReserveSlip($branchcode, $borrowernumber, $biblionumber)
2235
2236   Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
2237
2238 =cut
2239
2240 sub ReserveSlip {
2241     my ($branch, $borrowernumber, $biblionumber) = @_;
2242
2243 #   return unless ( C4::Context->boolean_preference('printreserveslips') );
2244
2245     my $reserve_id = GetReserveId({
2246         biblionumber => $biblionumber,
2247         borrowernumber => $borrowernumber
2248     }) or return;
2249     my $reserve = GetReserveInfo($reserve_id) or return;
2250
2251     return  C4::Letters::GetPreparedLetter (
2252         module => 'circulation',
2253         letter_code => 'RESERVESLIP',
2254         branchcode => $branch,
2255         tables => {
2256             'reserves'    => $reserve,
2257             'branches'    => $reserve->{branchcode},
2258             'borrowers'   => $reserve->{borrowernumber},
2259             'biblio'      => $reserve->{biblionumber},
2260             'items'       => $reserve->{itemnumber},
2261         },
2262     );
2263 }
2264
2265 =head2 GetReservesControlBranch
2266
2267   my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
2268
2269   Return the branchcode to be used to determine which reserves
2270   policy applies to a transaction.
2271
2272   C<$item> is a hashref for an item. Only 'homebranch' is used.
2273
2274   C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
2275
2276 =cut
2277
2278 sub GetReservesControlBranch {
2279     my ( $item, $borrower ) = @_;
2280
2281     my $reserves_control = C4::Context->preference('ReservesControlBranch');
2282
2283     my $branchcode =
2284         ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
2285       : ( $reserves_control eq 'PatronLibrary' )   ? $borrower->{'branchcode'}
2286       :                                              undef;
2287
2288     return $branchcode;
2289 }
2290
2291 =head2 CalculatePriority
2292
2293     my $p = CalculatePriority($biblionumber, $resdate);
2294
2295 Calculate priority for a new reserve on biblionumber, placing it at
2296 the end of the line of all holds whose start date falls before
2297 the current system time and that are neither on the hold shelf
2298 or in transit.
2299
2300 The reserve date parameter is optional; if it is supplied, the
2301 priority is based on the set of holds whose start date falls before
2302 the parameter value.
2303
2304 After calculation of this priority, it is recommended to call
2305 _ShiftPriorityByDateAndPriority. Note that this is currently done in
2306 AddReserves.
2307
2308 =cut
2309
2310 sub CalculatePriority {
2311     my ( $biblionumber, $resdate ) = @_;
2312
2313     my $sql = q{
2314         SELECT COUNT(*) FROM reserves
2315         WHERE biblionumber = ?
2316         AND   priority > 0
2317         AND   (found IS NULL OR found = '')
2318     };
2319     #skip found==W or found==T (waiting or transit holds)
2320     if( $resdate ) {
2321         $sql.= ' AND ( reservedate <= ? )';
2322     }
2323     else {
2324         $sql.= ' AND ( reservedate < NOW() )';
2325     }
2326     my $dbh = C4::Context->dbh();
2327     my @row = $dbh->selectrow_array(
2328         $sql,
2329         undef,
2330         $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2331     );
2332
2333     return @row ? $row[0]+1 : 1;
2334 }
2335
2336 =head1 AUTHOR
2337
2338 Koha Development Team <http://koha-community.org/>
2339
2340 =cut
2341
2342 1;