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