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