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