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