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