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