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