Bug 14569: Typo borroewr|borow
[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 $charge = C4::Context->preference("ExpireReservesMaxPickUpDelayCharge");
1081         my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
1082
1083         my $today = dt_from_string();
1084
1085         my $query = "SELECT * FROM reserves WHERE TO_DAYS( NOW() ) - TO_DAYS( waitingdate ) > ? AND found = 'W' AND priority = 0";
1086         $sth = $dbh->prepare( $query );
1087         $sth->execute( $max_pickup_delay );
1088
1089         while ( my $res = $sth->fetchrow_hashref ) {
1090             my $do_cancel = 1;
1091             unless ( $cancel_on_holidays ) {
1092                 my $calendar = Koha::Calendar->new( branchcode => $res->{'branchcode'} );
1093                 my $is_holiday = $calendar->is_holiday( $today );
1094
1095                 if ( $is_holiday ) {
1096                     $do_cancel = 0;
1097                 }
1098             }
1099
1100             if ( $do_cancel ) {
1101                 if ( $charge ) {
1102                     manualinvoice($res->{'borrowernumber'}, $res->{'itemnumber'}, 'Hold waiting too long', 'F', $charge);
1103                 }
1104
1105                 CancelReserve({ reserve_id => $res->{'reserve_id'} });
1106             }
1107         }
1108     }
1109
1110 }
1111
1112 =head2 AutoUnsuspendReserves
1113
1114   AutoUnsuspendReserves();
1115
1116 Unsuspends all suspended reserves with a suspend_until date from before today.
1117
1118 =cut
1119
1120 sub AutoUnsuspendReserves {
1121
1122     my $dbh = C4::Context->dbh;
1123
1124     my $query = "UPDATE reserves SET suspend = 0, suspend_until = NULL WHERE DATE( suspend_until ) < DATE( CURDATE() )";
1125     my $sth = $dbh->prepare( $query );
1126     $sth->execute();
1127
1128 }
1129
1130 =head2 CancelReserve
1131
1132   CancelReserve({ reserve_id => $reserve_id, [ biblionumber => $biblionumber, borrowernumber => $borrrowernumber, itemnumber => $itemnumber ] });
1133
1134 Cancels a reserve.
1135
1136 =cut
1137
1138 sub CancelReserve {
1139     my ( $params ) = @_;
1140
1141     my $reserve_id = $params->{'reserve_id'};
1142     $reserve_id = GetReserveId( $params ) unless ( $reserve_id );
1143
1144     return unless ( $reserve_id );
1145
1146     my $dbh = C4::Context->dbh;
1147
1148     my $reserve = GetReserve( $reserve_id );
1149     if ($reserve) {
1150         my $query = "
1151             UPDATE reserves
1152             SET    cancellationdate = now(),
1153                    found            = Null,
1154                    priority         = 0
1155             WHERE  reserve_id = ?
1156         ";
1157         my $sth = $dbh->prepare($query);
1158         $sth->execute( $reserve_id );
1159
1160         $query = "
1161             INSERT INTO old_reserves
1162             SELECT * FROM reserves
1163             WHERE  reserve_id = ?
1164         ";
1165         $sth = $dbh->prepare($query);
1166         $sth->execute( $reserve_id );
1167
1168         $query = "
1169             DELETE FROM reserves
1170             WHERE  reserve_id = ?
1171         ";
1172         $sth = $dbh->prepare($query);
1173         $sth->execute( $reserve_id );
1174
1175         # now fix the priority on the others....
1176         _FixPriority({ biblionumber => $reserve->{biblionumber} });
1177     }
1178
1179     return $reserve;
1180 }
1181
1182 =head2 ModReserve
1183
1184   ModReserve({ rank => $rank,
1185                reserve_id => $reserve_id,
1186                branchcode => $branchcode
1187                [, itemnumber => $itemnumber ]
1188                [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
1189               });
1190
1191 Change a hold request's priority or cancel it.
1192
1193 C<$rank> specifies the effect of the change.  If C<$rank>
1194 is 'W' or 'n', nothing happens.  This corresponds to leaving a
1195 request alone when changing its priority in the holds queue
1196 for a bib.
1197
1198 If C<$rank> is 'del', the hold request is cancelled.
1199
1200 If C<$rank> is an integer greater than zero, the priority of
1201 the request is set to that value.  Since priority != 0 means
1202 that the item is not waiting on the hold shelf, setting the
1203 priority to a non-zero value also sets the request's found
1204 status and waiting date to NULL.
1205
1206 The optional C<$itemnumber> parameter is used only when
1207 C<$rank> is a non-zero integer; if supplied, the itemnumber
1208 of the hold request is set accordingly; if omitted, the itemnumber
1209 is cleared.
1210
1211 B<FIXME:> Note that the forgoing can have the effect of causing
1212 item-level hold requests to turn into title-level requests.  This
1213 will be fixed once reserves has separate columns for requested
1214 itemnumber and supplying itemnumber.
1215
1216 =cut
1217
1218 sub ModReserve {
1219     my ( $params ) = @_;
1220
1221     my $rank = $params->{'rank'};
1222     my $reserve_id = $params->{'reserve_id'};
1223     my $branchcode = $params->{'branchcode'};
1224     my $itemnumber = $params->{'itemnumber'};
1225     my $suspend_until = $params->{'suspend_until'};
1226     my $borrowernumber = $params->{'borrowernumber'};
1227     my $biblionumber = $params->{'biblionumber'};
1228
1229     return if $rank eq "W";
1230     return if $rank eq "n";
1231
1232     return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
1233     $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber }) unless ( $reserve_id );
1234
1235     my $dbh = C4::Context->dbh;
1236     if ( $rank eq "del" ) {
1237         CancelReserve({ reserve_id => $reserve_id });
1238     }
1239     elsif ($rank =~ /^\d+/ and $rank > 0) {
1240         my $query = "
1241             UPDATE reserves SET priority = ? ,branchcode = ?, itemnumber = ?, found = NULL, waitingdate = NULL
1242             WHERE reserve_id = ?
1243         ";
1244         my $sth = $dbh->prepare($query);
1245         $sth->execute( $rank, $branchcode, $itemnumber, $reserve_id );
1246
1247         if ( defined( $suspend_until ) ) {
1248             if ( $suspend_until ) {
1249                 $suspend_until = C4::Dates->new( $suspend_until )->output("iso");
1250                 $dbh->do("UPDATE reserves SET suspend = 1, suspend_until = ? WHERE reserve_id = ?", undef, ( $suspend_until, $reserve_id ) );
1251             } else {
1252                 $dbh->do("UPDATE reserves SET suspend_until = NULL WHERE reserve_id = ?", undef, ( $reserve_id ) );
1253             }
1254         }
1255
1256         _FixPriority({ reserve_id => $reserve_id, rank =>$rank });
1257     }
1258 }
1259
1260 =head2 ModReserveFill
1261
1262   &ModReserveFill($reserve);
1263
1264 Fill a reserve. If I understand this correctly, this means that the
1265 reserved book has been found and given to the patron who reserved it.
1266
1267 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
1268 whose keys are fields from the reserves table in the Koha database.
1269
1270 =cut
1271
1272 sub ModReserveFill {
1273     my ($res) = @_;
1274     my $dbh = C4::Context->dbh;
1275     # fill in a reserve record....
1276     my $reserve_id = $res->{'reserve_id'};
1277     my $biblionumber = $res->{'biblionumber'};
1278     my $borrowernumber    = $res->{'borrowernumber'};
1279     my $resdate = $res->{'reservedate'};
1280
1281     # get the priority on this record....
1282     my $priority;
1283     my $query = "SELECT priority
1284                  FROM   reserves
1285                  WHERE  biblionumber   = ?
1286                   AND   borrowernumber = ?
1287                   AND   reservedate    = ?";
1288     my $sth = $dbh->prepare($query);
1289     $sth->execute( $biblionumber, $borrowernumber, $resdate );
1290     ($priority) = $sth->fetchrow_array;
1291
1292     # update the database...
1293     $query = "UPDATE reserves
1294                   SET    found            = 'F',
1295                          priority         = 0
1296                  WHERE  biblionumber     = ?
1297                     AND reservedate      = ?
1298                     AND borrowernumber   = ?
1299                 ";
1300     $sth = $dbh->prepare($query);
1301     $sth->execute( $biblionumber, $resdate, $borrowernumber );
1302
1303     # move to old_reserves
1304     $query = "INSERT INTO old_reserves
1305                  SELECT * FROM reserves
1306                  WHERE  biblionumber     = ?
1307                     AND reservedate      = ?
1308                     AND borrowernumber   = ?
1309                 ";
1310     $sth = $dbh->prepare($query);
1311     $sth->execute( $biblionumber, $resdate, $borrowernumber );
1312     $query = "DELETE FROM reserves
1313                  WHERE  biblionumber     = ?
1314                     AND reservedate      = ?
1315                     AND borrowernumber   = ?
1316                 ";
1317     $sth = $dbh->prepare($query);
1318     $sth->execute( $biblionumber, $resdate, $borrowernumber );
1319
1320     # now fix the priority on the others (if the priority wasn't
1321     # already sorted!)....
1322     unless ( $priority == 0 ) {
1323         _FixPriority({ reserve_id => $reserve_id, biblionumber => $biblionumber });
1324     }
1325 }
1326
1327 =head2 ModReserveStatus
1328
1329   &ModReserveStatus($itemnumber, $newstatus);
1330
1331 Update the reserve status for the active (priority=0) reserve.
1332
1333 $itemnumber is the itemnumber the reserve is on
1334
1335 $newstatus is the new status.
1336
1337 =cut
1338
1339 sub ModReserveStatus {
1340
1341     #first : check if we have a reservation for this item .
1342     my ($itemnumber, $newstatus) = @_;
1343     my $dbh = C4::Context->dbh;
1344
1345     my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1346     my $sth_set = $dbh->prepare($query);
1347     $sth_set->execute( $newstatus, $itemnumber );
1348
1349     if ( C4::Context->preference("ReturnToShelvingCart") && $newstatus ) {
1350       CartToShelf( $itemnumber );
1351     }
1352 }
1353
1354 =head2 ModReserveAffect
1355
1356   &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend);
1357
1358 This function affect an item and a status for a given reserve
1359 The itemnumber parameter is used to find the biblionumber.
1360 with the biblionumber & the borrowernumber, we can affect the itemnumber
1361 to the correct reserve.
1362
1363 if $transferToDo is not set, then the status is set to "Waiting" as well.
1364 otherwise, a transfer is on the way, and the end of the transfer will
1365 take care of the waiting status
1366
1367 =cut
1368
1369 sub ModReserveAffect {
1370     my ( $itemnumber, $borrowernumber,$transferToDo ) = @_;
1371     my $dbh = C4::Context->dbh;
1372
1373     # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1374     # attached to $itemnumber
1375     my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1376     $sth->execute($itemnumber);
1377     my ($biblionumber) = $sth->fetchrow;
1378
1379     # get request - need to find out if item is already
1380     # waiting in order to not send duplicate hold filled notifications
1381     my $reserve_id = GetReserveId({
1382         borrowernumber => $borrowernumber,
1383         biblionumber   => $biblionumber,
1384     });
1385     return unless defined $reserve_id;
1386     my $request = GetReserveInfo($reserve_id);
1387     my $already_on_shelf = ($request && $request->{found} eq 'W') ? 1 : 0;
1388
1389     # If we affect a reserve that has to be transferred, don't set to Waiting
1390     my $query;
1391     if ($transferToDo) {
1392     $query = "
1393         UPDATE reserves
1394         SET    priority = 0,
1395                itemnumber = ?,
1396                found = 'T'
1397         WHERE borrowernumber = ?
1398           AND biblionumber = ?
1399     ";
1400     }
1401     else {
1402     # affect the reserve to Waiting as well.
1403         $query = "
1404             UPDATE reserves
1405             SET     priority = 0,
1406                     found = 'W',
1407                     waitingdate = NOW(),
1408                     itemnumber = ?
1409             WHERE borrowernumber = ?
1410               AND biblionumber = ?
1411         ";
1412     }
1413     $sth = $dbh->prepare($query);
1414     $sth->execute( $itemnumber, $borrowernumber,$biblionumber);
1415     _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber ) if ( !$transferToDo && !$already_on_shelf );
1416     _FixPriority( { biblionumber => $biblionumber } );
1417     if ( C4::Context->preference("ReturnToShelvingCart") ) {
1418       CartToShelf( $itemnumber );
1419     }
1420
1421     return;
1422 }
1423
1424 =head2 ModReserveCancelAll
1425
1426   ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1427
1428 function to cancel reserv,check other reserves, and transfer document if it's necessary
1429
1430 =cut
1431
1432 sub ModReserveCancelAll {
1433     my $messages;
1434     my $nextreservinfo;
1435     my ( $itemnumber, $borrowernumber ) = @_;
1436
1437     #step 1 : cancel the reservation
1438     my $CancelReserve = CancelReserve({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1439
1440     #step 2 launch the subroutine of the others reserves
1441     ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1442
1443     return ( $messages, $nextreservinfo );
1444 }
1445
1446 =head2 ModReserveMinusPriority
1447
1448   &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1449
1450 Reduce the values of queued list
1451
1452 =cut
1453
1454 sub ModReserveMinusPriority {
1455     my ( $itemnumber, $reserve_id ) = @_;
1456
1457     #first step update the value of the first person on reserv
1458     my $dbh   = C4::Context->dbh;
1459     my $query = "
1460         UPDATE reserves
1461         SET    priority = 0 , itemnumber = ? 
1462         WHERE  reserve_id = ?
1463     ";
1464     my $sth_upd = $dbh->prepare($query);
1465     $sth_upd->execute( $itemnumber, $reserve_id );
1466     # second step update all others reserves
1467     _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1468 }
1469
1470 =head2 GetReserveInfo
1471
1472   &GetReserveInfo($reserve_id);
1473
1474 Get item and borrower details for a current hold.
1475 Current implementation this query should have a single result.
1476
1477 =cut
1478
1479 sub GetReserveInfo {
1480     my ( $reserve_id ) = @_;
1481     my $dbh = C4::Context->dbh;
1482     my $strsth="SELECT
1483                    reserve_id,
1484                    reservedate,
1485                    reservenotes,
1486                    reserves.borrowernumber,
1487                    reserves.biblionumber,
1488                    reserves.branchcode,
1489                    reserves.waitingdate,
1490                    notificationdate,
1491                    reminderdate,
1492                    priority,
1493                    found,
1494                    firstname,
1495                    surname,
1496                    phone,
1497                    email,
1498                    address,
1499                    address2,
1500                    cardnumber,
1501                    city,
1502                    zipcode,
1503                    biblio.title,
1504                    biblio.author,
1505                    items.holdingbranch,
1506                    items.itemcallnumber,
1507                    items.itemnumber,
1508                    items.location,
1509                    barcode,
1510                    notes
1511                 FROM reserves
1512                 LEFT JOIN items USING(itemnumber)
1513                 LEFT JOIN borrowers USING(borrowernumber)
1514                 LEFT JOIN biblio ON  (reserves.biblionumber=biblio.biblionumber)
1515                 WHERE reserves.reserve_id = ?";
1516     my $sth = $dbh->prepare($strsth);
1517     $sth->execute($reserve_id);
1518
1519     my $data = $sth->fetchrow_hashref;
1520     return $data;
1521 }
1522
1523 =head2 IsAvailableForItemLevelRequest
1524
1525   my $is_available = IsAvailableForItemLevelRequest($item_record,$borrower_record);
1526
1527 Checks whether a given item record is available for an
1528 item-level hold request.  An item is available if
1529
1530 * it is not lost AND
1531 * it is not damaged AND
1532 * it is not withdrawn AND
1533 * does not have a not for loan value > 0
1534
1535 Need to check the issuingrules onshelfholds column,
1536 if this is set items on the shelf can be placed on hold
1537
1538 Note that IsAvailableForItemLevelRequest() does not
1539 check if the staff operator is authorized to place
1540 a request on the item - in particular,
1541 this routine does not check IndependentBranches
1542 and canreservefromotherbranches.
1543
1544 =cut
1545
1546 sub IsAvailableForItemLevelRequest {
1547     my $item = shift;
1548     my $borrower = shift;
1549
1550     my $dbh = C4::Context->dbh;
1551     # must check the notforloan setting of the itemtype
1552     # FIXME - a lot of places in the code do this
1553     #         or something similar - need to be
1554     #         consolidated
1555     my $itype = _get_itype($item);
1556     my $notforloan_per_itemtype
1557       = $dbh->selectrow_array("SELECT notforloan FROM itemtypes WHERE itemtype = ?",
1558                               undef, $itype);
1559
1560     return 0 if
1561         $notforloan_per_itemtype ||
1562         $item->{itemlost}        ||
1563         $item->{notforloan} > 0  ||
1564         $item->{withdrawn}        ||
1565         ($item->{damaged} && !C4::Context->preference('AllowHoldsOnDamagedItems'));
1566
1567
1568     return 1 if _OnShelfHoldsAllowed($itype,$borrower->{categorycode},$item->{holdingbranch});
1569
1570     return $item->{onloan} || GetReserveStatus($item->{itemnumber}) eq "Waiting";
1571 }
1572
1573 =head2 OnShelfHoldsAllowed
1574
1575   OnShelfHoldsAllowed($itemtype,$borrowercategory,$branchcode);
1576
1577 Checks issuingrules, using the borrowers categorycode, the itemtype, and branchcode to see if onshelf
1578 holds are allowed, returns true if so.
1579
1580 =cut
1581
1582 sub OnShelfHoldsAllowed {
1583     my ($item, $borrower) = @_;
1584
1585     my $itype = _get_itype($item);
1586     return _OnShelfHoldsAllowed($itype,$borrower->{categorycode},$item->{holdingbranch});
1587 }
1588
1589 sub _get_itype {
1590     my $item = shift;
1591
1592     my $itype;
1593     if (C4::Context->preference('item-level_itypes')) {
1594         # We can't trust GetItem to honour the syspref, so safest to do it ourselves
1595         # When GetItem is fixed, we can remove this
1596         $itype = $item->{itype};
1597     }
1598     else {
1599         # XXX This is a bit dodgy. It relies on biblio itemtype column having different name.
1600         # So if we already have a biblioitems join when calling this function,
1601         # we don't need to access the database again
1602         $itype = $item->{itemtype};
1603     }
1604     unless ($itype) {
1605         my $dbh = C4::Context->dbh;
1606         my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? ";
1607         my $sth = $dbh->prepare($query);
1608         $sth->execute($item->{biblioitemnumber});
1609         if (my $data = $sth->fetchrow_hashref()){
1610             $itype = $data->{itemtype};
1611         }
1612     }
1613     return $itype;
1614 }
1615
1616 sub _OnShelfHoldsAllowed {
1617     my ($itype,$borrowercategory,$branchcode) = @_;
1618
1619     my $rule = C4::Circulation::GetIssuingRule($borrowercategory, $itype, $branchcode);
1620     return $rule->{onshelfholds};
1621 }
1622
1623 =head2 AlterPriority
1624
1625   AlterPriority( $where, $reserve_id );
1626
1627 This function changes a reserve's priority up, down, to the top, or to the bottom.
1628 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1629
1630 =cut
1631
1632 sub AlterPriority {
1633     my ( $where, $reserve_id ) = @_;
1634
1635     my $dbh = C4::Context->dbh;
1636
1637     my $reserve = GetReserve( $reserve_id );
1638
1639     if ( $reserve->{cancellationdate} ) {
1640         warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (".$reserve->{cancellationdate}.')';
1641         return;
1642     }
1643
1644     if ( $where eq 'up' || $where eq 'down' ) {
1645
1646       my $priority = $reserve->{'priority'};
1647       $priority = $where eq 'up' ? $priority - 1 : $priority + 1;
1648       _FixPriority({ reserve_id => $reserve_id, rank => $priority })
1649
1650     } elsif ( $where eq 'top' ) {
1651
1652       _FixPriority({ reserve_id => $reserve_id, rank => '1' })
1653
1654     } elsif ( $where eq 'bottom' ) {
1655
1656       _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1657
1658     }
1659 }
1660
1661 =head2 ToggleLowestPriority
1662
1663   ToggleLowestPriority( $borrowernumber, $biblionumber );
1664
1665 This function sets the lowestPriority field to true if is false, and false if it is true.
1666
1667 =cut
1668
1669 sub ToggleLowestPriority {
1670     my ( $reserve_id ) = @_;
1671
1672     my $dbh = C4::Context->dbh;
1673
1674     my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1675     $sth->execute( $reserve_id );
1676     
1677     _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1678 }
1679
1680 =head2 ToggleSuspend
1681
1682   ToggleSuspend( $reserve_id );
1683
1684 This function sets the suspend field to true if is false, and false if it is true.
1685 If the reserve is currently suspended with a suspend_until date, that date will
1686 be cleared when it is unsuspended.
1687
1688 =cut
1689
1690 sub ToggleSuspend {
1691     my ( $reserve_id, $suspend_until ) = @_;
1692
1693     $suspend_until = output_pref(
1694         {
1695             dt         => dt_from_string($suspend_until),
1696             dateformat => 'iso',
1697             dateonly   => 1
1698         }
1699     ) if ($suspend_until);
1700
1701     my $do_until = ( $suspend_until ) ? '?' : 'NULL';
1702
1703     my $dbh = C4::Context->dbh;
1704
1705     my $sth = $dbh->prepare(
1706         "UPDATE reserves SET suspend = NOT suspend,
1707         suspend_until = CASE WHEN suspend = 0 THEN NULL ELSE $do_until END
1708         WHERE reserve_id = ?
1709     ");
1710
1711     my @params;
1712     push( @params, $suspend_until ) if ( $suspend_until );
1713     push( @params, $reserve_id );
1714
1715     $sth->execute( @params );
1716 }
1717
1718 =head2 SuspendAll
1719
1720   SuspendAll(
1721       borrowernumber   => $borrowernumber,
1722       [ biblionumber   => $biblionumber, ]
1723       [ suspend_until  => $suspend_until, ]
1724       [ suspend        => $suspend ]
1725   );
1726
1727   This function accepts a set of hash keys as its parameters.
1728   It requires either borrowernumber or biblionumber, or both.
1729
1730   suspend_until is wholly optional.
1731
1732 =cut
1733
1734 sub SuspendAll {
1735     my %params = @_;
1736
1737     my $borrowernumber = $params{'borrowernumber'} || undef;
1738     my $biblionumber   = $params{'biblionumber'}   || undef;
1739     my $suspend_until  = $params{'suspend_until'}  || undef;
1740     my $suspend        = defined( $params{'suspend'} ) ? $params{'suspend'} :  1;
1741
1742     $suspend_until = C4::Dates->new( $suspend_until )->output("iso") if ( defined( $suspend_until ) );
1743
1744     return unless ( $borrowernumber || $biblionumber );
1745
1746     my ( $query, $sth, $dbh, @query_params );
1747
1748     $query = "UPDATE reserves SET suspend = ? ";
1749     push( @query_params, $suspend );
1750     if ( !$suspend ) {
1751         $query .= ", suspend_until = NULL ";
1752     } elsif ( $suspend_until ) {
1753         $query .= ", suspend_until = ? ";
1754         push( @query_params, $suspend_until );
1755     }
1756     $query .= " WHERE ";
1757     if ( $borrowernumber ) {
1758         $query .= " borrowernumber = ? ";
1759         push( @query_params, $borrowernumber );
1760     }
1761     $query .= " AND " if ( $borrowernumber && $biblionumber );
1762     if ( $biblionumber ) {
1763         $query .= " biblionumber = ? ";
1764         push( @query_params, $biblionumber );
1765     }
1766     $query .= " AND found IS NULL ";
1767
1768     $dbh = C4::Context->dbh;
1769     $sth = $dbh->prepare( $query );
1770     $sth->execute( @query_params );
1771 }
1772
1773
1774 =head2 _FixPriority
1775
1776   _FixPriority({
1777     reserve_id => $reserve_id,
1778     [rank => $rank,]
1779     [ignoreSetLowestRank => $ignoreSetLowestRank]
1780   });
1781
1782   or
1783
1784   _FixPriority({ biblionumber => $biblionumber});
1785
1786 This routine adjusts the priority of a hold request and holds
1787 on the same bib.
1788
1789 In the first form, where a reserve_id is passed, the priority of the
1790 hold is set to supplied rank, and other holds for that bib are adjusted
1791 accordingly.  If the rank is "del", the hold is cancelled.  If no rank
1792 is supplied, all of the holds on that bib have their priority adjusted
1793 as if the second form had been used.
1794
1795 In the second form, where a biblionumber is passed, the holds on that
1796 bib (that are not captured) are sorted in order of increasing priority,
1797 then have reserves.priority set so that the first non-captured hold
1798 has its priority set to 1, the second non-captured hold has its priority
1799 set to 2, and so forth.
1800
1801 In both cases, holds that have the lowestPriority flag on are have their
1802 priority adjusted to ensure that they remain at the end of the line.
1803
1804 Note that the ignoreSetLowestRank parameter is meant to be used only
1805 when _FixPriority calls itself.
1806
1807 =cut
1808
1809 sub _FixPriority {
1810     my ( $params ) = @_;
1811     my $reserve_id = $params->{reserve_id};
1812     my $rank = $params->{rank} // '';
1813     my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1814     my $biblionumber = $params->{biblionumber};
1815
1816     my $dbh = C4::Context->dbh;
1817
1818     unless ( $biblionumber ) {
1819         my $res = GetReserve( $reserve_id );
1820         $biblionumber = $res->{biblionumber};
1821     }
1822
1823     if ( $rank eq "del" ) {
1824          CancelReserve({ reserve_id => $reserve_id });
1825     }
1826     elsif ( $rank eq "W" || $rank eq "0" ) {
1827
1828         # make sure priority for waiting or in-transit items is 0
1829         my $query = "
1830             UPDATE reserves
1831             SET    priority = 0
1832             WHERE reserve_id = ?
1833             AND found IN ('W', 'T')
1834         ";
1835         my $sth = $dbh->prepare($query);
1836         $sth->execute( $reserve_id );
1837     }
1838     my @priority;
1839
1840     # get whats left
1841     my $query = "
1842         SELECT reserve_id, borrowernumber, reservedate, constrainttype
1843         FROM   reserves
1844         WHERE  biblionumber   = ?
1845           AND  ((found <> 'W' AND found <> 'T') OR found IS NULL)
1846         ORDER BY priority ASC
1847     ";
1848     my $sth = $dbh->prepare($query);
1849     $sth->execute( $biblionumber );
1850     while ( my $line = $sth->fetchrow_hashref ) {
1851         push( @priority,     $line );
1852     }
1853
1854     # To find the matching index
1855     my $i;
1856     my $key = -1;    # to allow for 0 to be a valid result
1857     for ( $i = 0 ; $i < @priority ; $i++ ) {
1858         if ( $reserve_id == $priority[$i]->{'reserve_id'} ) {
1859             $key = $i;    # save the index
1860             last;
1861         }
1862     }
1863
1864     # if index exists in array then move it to new position
1865     if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1866         my $new_rank = $rank -
1867           1;    # $new_rank is what you want the new index to be in the array
1868         my $moving_item = splice( @priority, $key, 1 );
1869         splice( @priority, $new_rank, 0, $moving_item );
1870     }
1871
1872     # now fix the priority on those that are left....
1873     $query = "
1874         UPDATE reserves
1875         SET    priority = ?
1876         WHERE  reserve_id = ?
1877     ";
1878     $sth = $dbh->prepare($query);
1879     for ( my $j = 0 ; $j < @priority ; $j++ ) {
1880         $sth->execute(
1881             $j + 1,
1882             $priority[$j]->{'reserve_id'}
1883         );
1884     }
1885     
1886     $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1887     $sth->execute();
1888
1889     unless ( $ignoreSetLowestRank ) {
1890       while ( my $res = $sth->fetchrow_hashref() ) {
1891         _FixPriority({
1892             reserve_id => $res->{'reserve_id'},
1893             rank => '999999',
1894             ignoreSetLowestRank => 1
1895         });
1896       }
1897     }
1898 }
1899
1900 =head2 _Findgroupreserve
1901
1902   @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead, $ignore_borrowers);
1903
1904 Looks for an item-specific match first, then for a title-level match, returning the
1905 first match found.  If neither, then we look for a 3rd kind of match based on
1906 reserve constraints.
1907 Lookahead is the number of days to look in advance.
1908
1909 TODO: add more explanation about reserve constraints
1910
1911 C<&_Findgroupreserve> returns :
1912 C<@results> is an array of references-to-hash whose keys are mostly
1913 fields from the reserves table of the Koha database, plus
1914 C<biblioitemnumber>.
1915
1916 =cut
1917
1918 sub _Findgroupreserve {
1919     my ( $bibitem, $biblio, $itemnumber, $lookahead, $ignore_borrowers) = @_;
1920     my $dbh   = C4::Context->dbh;
1921
1922     # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1923     # check for exact targeted match
1924     my $item_level_target_query = qq{
1925         SELECT reserves.biblionumber        AS biblionumber,
1926                reserves.borrowernumber      AS borrowernumber,
1927                reserves.reservedate         AS reservedate,
1928                reserves.branchcode          AS branchcode,
1929                reserves.cancellationdate    AS cancellationdate,
1930                reserves.found               AS found,
1931                reserves.reservenotes        AS reservenotes,
1932                reserves.priority            AS priority,
1933                reserves.timestamp           AS timestamp,
1934                biblioitems.biblioitemnumber AS biblioitemnumber,
1935                reserves.itemnumber          AS itemnumber,
1936                reserves.reserve_id          AS reserve_id
1937         FROM reserves
1938         JOIN biblioitems USING (biblionumber)
1939         JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1940         WHERE found IS NULL
1941         AND priority > 0
1942         AND item_level_request = 1
1943         AND itemnumber = ?
1944         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1945         AND suspend = 0
1946         ORDER BY priority
1947     };
1948     my $sth = $dbh->prepare($item_level_target_query);
1949     $sth->execute($itemnumber, $lookahead||0);
1950     my @results;
1951     if ( my $data = $sth->fetchrow_hashref ) {
1952         push( @results, $data )
1953           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1954     }
1955     return @results if @results;
1956
1957     # check for title-level targeted match
1958     my $title_level_target_query = qq{
1959         SELECT reserves.biblionumber        AS biblionumber,
1960                reserves.borrowernumber      AS borrowernumber,
1961                reserves.reservedate         AS reservedate,
1962                reserves.branchcode          AS branchcode,
1963                reserves.cancellationdate    AS cancellationdate,
1964                reserves.found               AS found,
1965                reserves.reservenotes        AS reservenotes,
1966                reserves.priority            AS priority,
1967                reserves.timestamp           AS timestamp,
1968                biblioitems.biblioitemnumber AS biblioitemnumber,
1969                reserves.itemnumber          AS itemnumber,
1970                reserves.reserve_id          AS reserve_id
1971         FROM reserves
1972         JOIN biblioitems USING (biblionumber)
1973         JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1974         WHERE found IS NULL
1975         AND priority > 0
1976         AND item_level_request = 0
1977         AND hold_fill_targets.itemnumber = ?
1978         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1979         AND suspend = 0
1980         ORDER BY priority
1981     };
1982     $sth = $dbh->prepare($title_level_target_query);
1983     $sth->execute($itemnumber, $lookahead||0);
1984     @results = ();
1985     if ( my $data = $sth->fetchrow_hashref ) {
1986         push( @results, $data )
1987           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1988     }
1989     return @results if @results;
1990
1991     my $query = qq{
1992         SELECT reserves.biblionumber               AS biblionumber,
1993                reserves.borrowernumber             AS borrowernumber,
1994                reserves.reservedate                AS reservedate,
1995                reserves.waitingdate                AS waitingdate,
1996                reserves.branchcode                 AS branchcode,
1997                reserves.cancellationdate           AS cancellationdate,
1998                reserves.found                      AS found,
1999                reserves.reservenotes               AS reservenotes,
2000                reserves.priority                   AS priority,
2001                reserves.timestamp                  AS timestamp,
2002                reserveconstraints.biblioitemnumber AS biblioitemnumber,
2003                reserves.itemnumber                 AS itemnumber,
2004                reserves.reserve_id                 AS reserve_id
2005         FROM reserves
2006           LEFT JOIN reserveconstraints ON reserves.biblionumber = reserveconstraints.biblionumber
2007         WHERE reserves.biblionumber = ?
2008           AND ( ( reserveconstraints.biblioitemnumber = ?
2009           AND reserves.borrowernumber = reserveconstraints.borrowernumber
2010           AND reserves.reservedate    = reserveconstraints.reservedate )
2011           OR  reserves.constrainttype='a' )
2012           AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
2013           AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
2014           AND suspend = 0
2015           ORDER BY priority
2016     };
2017     $sth = $dbh->prepare($query);
2018     $sth->execute( $biblio, $bibitem, $itemnumber, $lookahead||0);
2019     @results = ();
2020     while ( my $data = $sth->fetchrow_hashref ) {
2021         push( @results, $data )
2022           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
2023     }
2024     return @results;
2025 }
2026
2027 =head2 _koha_notify_reserve
2028
2029   _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber );
2030
2031 Sends a notification to the patron that their hold has been filled (through
2032 ModReserveAffect, _not_ ModReserveFill)
2033
2034 =cut
2035
2036 sub _koha_notify_reserve {
2037     my ($itemnumber, $borrowernumber, $biblionumber) = @_;
2038
2039     my $dbh = C4::Context->dbh;
2040     my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
2041
2042     # Try to get the borrower's email address
2043     my $to_address = C4::Members::GetNoticeEmailAddress($borrowernumber);
2044
2045     my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
2046             borrowernumber => $borrowernumber,
2047             message_name => 'Hold_Filled'
2048     } );
2049
2050     my $sth = $dbh->prepare("
2051         SELECT *
2052         FROM   reserves
2053         WHERE  borrowernumber = ?
2054             AND biblionumber = ?
2055     ");
2056     $sth->execute( $borrowernumber, $biblionumber );
2057     my $reserve = $sth->fetchrow_hashref;
2058     my $branch_details = GetBranchDetail( $reserve->{'branchcode'} );
2059
2060     my $admin_email_address = $branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
2061
2062     my %letter_params = (
2063         module => 'reserves',
2064         branchcode => $reserve->{branchcode},
2065         tables => {
2066             'branches'  => $branch_details,
2067             'borrowers' => $borrower,
2068             'biblio'    => $biblionumber,
2069             'reserves'  => $reserve,
2070             'items', $reserve->{'itemnumber'},
2071         },
2072         substitute => { today => C4::Dates->new()->output() },
2073     );
2074
2075     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.
2076     my $send_notification = sub {
2077         my ( $mtt, $letter_code ) = (@_);
2078         return unless defined $letter_code;
2079         $letter_params{letter_code} = $letter_code;
2080         $letter_params{message_transport_type} = $mtt;
2081         my $letter =  C4::Letters::GetPreparedLetter ( %letter_params );
2082         unless ($letter) {
2083             warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
2084             return;
2085         }
2086
2087         C4::Letters::EnqueueLetter( {
2088             letter => $letter,
2089             borrowernumber => $borrowernumber,
2090             from_address => $admin_email_address,
2091             message_transport_type => $mtt,
2092         } );
2093     };
2094
2095     while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
2096         next if (
2097                ( $mtt eq 'email' and not $to_address ) # No email address
2098             or ( $mtt eq 'sms'   and not $borrower->{smsalertnumber} ) # No SMS number
2099             or ( $mtt eq 'phone' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl
2100         );
2101
2102         &$send_notification($mtt, $letter_code);
2103         $notification_sent++;
2104     }
2105     #Making sure that a print notification is sent if no other transport types can be utilized.
2106     if (! $notification_sent) {
2107         &$send_notification('print', 'HOLD');
2108     }
2109     
2110 }
2111
2112 =head2 _ShiftPriorityByDateAndPriority
2113
2114   $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
2115
2116 This increments the priority of all reserves after the one
2117 with either the lowest date after C<$reservedate>
2118 or the lowest priority after C<$priority>.
2119
2120 It effectively makes room for a new reserve to be inserted with a certain
2121 priority, which is returned.
2122
2123 This is most useful when the reservedate can be set by the user.  It allows
2124 the new reserve to be placed before other reserves that have a later
2125 reservedate.  Since priority also is set by the form in reserves/request.pl
2126 the sub accounts for that too.
2127
2128 =cut
2129
2130 sub _ShiftPriorityByDateAndPriority {
2131     my ( $biblio, $resdate, $new_priority ) = @_;
2132
2133     my $dbh = C4::Context->dbh;
2134     my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
2135     my $sth = $dbh->prepare( $query );
2136     $sth->execute( $biblio, $resdate, $new_priority );
2137     my $min_priority = $sth->fetchrow;
2138     # if no such matches are found, $new_priority remains as original value
2139     $new_priority = $min_priority if ( $min_priority );
2140
2141     # Shift the priority up by one; works in conjunction with the next SQL statement
2142     $query = "UPDATE reserves
2143               SET priority = priority+1
2144               WHERE biblionumber = ?
2145               AND borrowernumber = ?
2146               AND reservedate = ?
2147               AND found IS NULL";
2148     my $sth_update = $dbh->prepare( $query );
2149
2150     # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
2151     $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
2152     $sth = $dbh->prepare( $query );
2153     $sth->execute( $new_priority, $biblio );
2154     while ( my $row = $sth->fetchrow_hashref ) {
2155         $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
2156     }
2157
2158     return $new_priority;  # so the caller knows what priority they wind up receiving
2159 }
2160
2161 =head2 OPACItemHoldsAllowed
2162
2163   OPACItemHoldsAllowed($item_record,$borrower_record);
2164
2165 Checks issuingrules, using the borrowers categorycode, the itemtype, and branchcode to see
2166 if specific item holds are allowed, returns true if so.
2167
2168 =cut
2169
2170 sub OPACItemHoldsAllowed {
2171     my ($item,$borrower) = @_;
2172
2173     my $branchcode = $item->{homebranch} or die "No homebranch";
2174     my $itype;
2175     my $dbh = C4::Context->dbh;
2176     if (C4::Context->preference('item-level_itypes')) {
2177        # We can't trust GetItem to honour the syspref, so safest to do it ourselves
2178        # When GetItem is fixed, we can remove this
2179        $itype = $item->{itype};
2180     }
2181     else {
2182        my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? ";
2183        my $sth = $dbh->prepare($query);
2184        $sth->execute($item->{biblioitemnumber});
2185        if (my $data = $sth->fetchrow_hashref()){
2186            $itype = $data->{itemtype};
2187        }
2188     }
2189
2190     my $query = "SELECT opacitemholds,categorycode,itemtype,branchcode FROM issuingrules WHERE
2191           (issuingrules.categorycode = ? OR issuingrules.categorycode = '*')
2192         AND
2193           (issuingrules.itemtype = ? OR issuingrules.itemtype = '*')
2194         AND
2195           (issuingrules.branchcode = ? OR issuingrules.branchcode = '*')
2196         ORDER BY
2197           issuingrules.categorycode desc,
2198           issuingrules.itemtype desc,
2199           issuingrules.branchcode desc
2200        LIMIT 1";
2201     my $sth = $dbh->prepare($query);
2202     $sth->execute($borrower->{categorycode},$itype,$branchcode);
2203     my $data = $sth->fetchrow_hashref;
2204     my $opacitemholds = uc substr ($data->{opacitemholds}, 0, 1);
2205     return '' if $opacitemholds eq 'N';
2206     return $opacitemholds;
2207 }
2208
2209 =head2 MoveReserve
2210
2211   MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
2212
2213 Use when checking out an item to handle reserves
2214 If $cancelreserve boolean is set to true, it will remove existing reserve
2215
2216 =cut
2217
2218 sub MoveReserve {
2219     my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
2220
2221     my ( $restype, $res, $all_reserves ) = CheckReserves( $itemnumber );
2222     return unless $res;
2223
2224     my $biblionumber     =  $res->{biblionumber};
2225     my $biblioitemnumber = $res->{biblioitemnumber};
2226
2227     if ($res->{borrowernumber} == $borrowernumber) {
2228         ModReserveFill($res);
2229     }
2230     else {
2231         # warn "Reserved";
2232         # The item is reserved by someone else.
2233         # Find this item in the reserves
2234
2235         my $borr_res;
2236         foreach (@$all_reserves) {
2237             $_->{'borrowernumber'} == $borrowernumber or next;
2238             $_->{'biblionumber'}   == $biblionumber   or next;
2239
2240             $borr_res = $_;
2241             last;
2242         }
2243
2244         if ( $borr_res ) {
2245             # The item is reserved by the current patron
2246             ModReserveFill($borr_res);
2247         }
2248
2249         if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
2250             RevertWaitingStatus({ itemnumber => $itemnumber });
2251         }
2252         elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
2253             CancelReserve({
2254                 biblionumber   => $res->{'biblionumber'},
2255                 itemnumber     => $res->{'itemnumber'},
2256                 borrowernumber => $res->{'borrowernumber'}
2257             });
2258         }
2259     }
2260 }
2261
2262 =head2 MergeHolds
2263
2264   MergeHolds($dbh,$to_biblio, $from_biblio);
2265
2266 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
2267
2268 =cut
2269
2270 sub MergeHolds {
2271     my ( $dbh, $to_biblio, $from_biblio ) = @_;
2272     my $sth = $dbh->prepare(
2273         "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
2274     );
2275     $sth->execute($from_biblio);
2276     if ( my $data = $sth->fetchrow_hashref() ) {
2277
2278         # holds exist on old record, if not we don't need to do anything
2279         $sth = $dbh->prepare(
2280             "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
2281         $sth->execute( $to_biblio, $from_biblio );
2282
2283         # Reorder by date
2284         # don't reorder those already waiting
2285
2286         $sth = $dbh->prepare(
2287 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
2288         );
2289         my $upd_sth = $dbh->prepare(
2290 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
2291         AND reservedate = ? AND constrainttype = ? AND (itemnumber = ? or itemnumber is NULL) "
2292         );
2293         $sth->execute( $to_biblio, 'W', 'T' );
2294         my $priority = 1;
2295         while ( my $reserve = $sth->fetchrow_hashref() ) {
2296             $upd_sth->execute(
2297                 $priority,                    $to_biblio,
2298                 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
2299                 $reserve->{'constrainttype'}, $reserve->{'itemnumber'}
2300             );
2301             $priority++;
2302         }
2303     }
2304 }
2305
2306 =head2 RevertWaitingStatus
2307
2308   RevertWaitingStatus({ itemnumber => $itemnumber });
2309
2310   Reverts a 'waiting' hold back to a regular hold with a priority of 1.
2311
2312   Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
2313           item level hold, even if it was only a bibliolevel hold to
2314           begin with. This is because we can no longer know if a hold
2315           was item-level or bib-level after a hold has been set to
2316           waiting status.
2317
2318 =cut
2319
2320 sub RevertWaitingStatus {
2321     my ( $params ) = @_;
2322     my $itemnumber = $params->{'itemnumber'};
2323
2324     return unless ( $itemnumber );
2325
2326     my $dbh = C4::Context->dbh;
2327
2328     ## Get the waiting reserve we want to revert
2329     my $query = "
2330         SELECT * FROM reserves
2331         WHERE itemnumber = ?
2332         AND found IS NOT NULL
2333     ";
2334     my $sth = $dbh->prepare( $query );
2335     $sth->execute( $itemnumber );
2336     my $reserve = $sth->fetchrow_hashref();
2337
2338     ## Increment the priority of all other non-waiting
2339     ## reserves for this bib record
2340     $query = "
2341         UPDATE reserves
2342         SET
2343           priority = priority + 1
2344         WHERE
2345           biblionumber =  ?
2346         AND
2347           priority > 0
2348     ";
2349     $sth = $dbh->prepare( $query );
2350     $sth->execute( $reserve->{'biblionumber'} );
2351
2352     ## Fix up the currently waiting reserve
2353     $query = "
2354     UPDATE reserves
2355     SET
2356       priority = 1,
2357       found = NULL,
2358       waitingdate = NULL
2359     WHERE
2360       reserve_id = ?
2361     ";
2362     $sth = $dbh->prepare( $query );
2363     $sth->execute( $reserve->{'reserve_id'} );
2364     _FixPriority( { biblionumber => $reserve->{biblionumber} } );
2365 }
2366
2367 =head2 GetReserveId
2368
2369   $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber [, itemnumber => $itemnumber ] });
2370
2371   Returnes the first reserve id that matches the given criteria
2372
2373 =cut
2374
2375 sub GetReserveId {
2376     my ( $params ) = @_;
2377
2378     return unless ( ( $params->{'biblionumber'} || $params->{'itemnumber'} ) && $params->{'borrowernumber'} );
2379
2380     my $dbh = C4::Context->dbh();
2381
2382     my $sql = "SELECT reserve_id FROM reserves WHERE ";
2383
2384     my @params;
2385     my @limits;
2386     foreach my $key ( keys %$params ) {
2387         if ( defined( $params->{$key} ) ) {
2388             push( @limits, "$key = ?" );
2389             push( @params, $params->{$key} );
2390         }
2391     }
2392
2393     $sql .= join( " AND ", @limits );
2394
2395     my $sth = $dbh->prepare( $sql );
2396     $sth->execute( @params );
2397     my $row = $sth->fetchrow_hashref();
2398
2399     return $row->{'reserve_id'};
2400 }
2401
2402 =head2 ReserveSlip
2403
2404   ReserveSlip($branchcode, $borrowernumber, $biblionumber)
2405
2406   Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
2407
2408 =cut
2409
2410 sub ReserveSlip {
2411     my ($branch, $borrowernumber, $biblionumber) = @_;
2412
2413 #   return unless ( C4::Context->boolean_preference('printreserveslips') );
2414
2415     my $reserve_id = GetReserveId({
2416         biblionumber => $biblionumber,
2417         borrowernumber => $borrowernumber
2418     }) or return;
2419     my $reserve = GetReserveInfo($reserve_id) or return;
2420
2421     return  C4::Letters::GetPreparedLetter (
2422         module => 'circulation',
2423         letter_code => 'RESERVESLIP',
2424         branchcode => $branch,
2425         tables => {
2426             'reserves'    => $reserve,
2427             'branches'    => $reserve->{branchcode},
2428             'borrowers'   => $reserve->{borrowernumber},
2429             'biblio'      => $reserve->{biblionumber},
2430             'items'       => $reserve->{itemnumber},
2431         },
2432     );
2433 }
2434
2435 =head2 GetReservesControlBranch
2436
2437   my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
2438
2439   Return the branchcode to be used to determine which reserves
2440   policy applies to a transaction.
2441
2442   C<$item> is a hashref for an item. Only 'homebranch' is used.
2443
2444   C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
2445
2446 =cut
2447
2448 sub GetReservesControlBranch {
2449     my ( $item, $borrower ) = @_;
2450
2451     my $reserves_control = C4::Context->preference('ReservesControlBranch');
2452
2453     my $branchcode =
2454         ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
2455       : ( $reserves_control eq 'PatronLibrary' )   ? $borrower->{'branchcode'}
2456       :                                              undef;
2457
2458     return $branchcode;
2459 }
2460
2461 =head2 CalculatePriority
2462
2463     my $p = CalculatePriority($biblionumber, $resdate);
2464
2465 Calculate priority for a new reserve on biblionumber, placing it at
2466 the end of the line of all holds whose start date falls before
2467 the current system time and that are neither on the hold shelf
2468 or in transit.
2469
2470 The reserve date parameter is optional; if it is supplied, the
2471 priority is based on the set of holds whose start date falls before
2472 the parameter value.
2473
2474 After calculation of this priority, it is recommended to call
2475 _ShiftPriorityByDateAndPriority. Note that this is currently done in
2476 AddReserves.
2477
2478 =cut
2479
2480 sub CalculatePriority {
2481     my ( $biblionumber, $resdate ) = @_;
2482
2483     my $sql = q{
2484         SELECT COUNT(*) FROM reserves
2485         WHERE biblionumber = ?
2486         AND   priority > 0
2487         AND   (found IS NULL OR found = '')
2488     };
2489     #skip found==W or found==T (waiting or transit holds)
2490     if( $resdate ) {
2491         $sql.= ' AND ( reservedate <= ? )';
2492     }
2493     else {
2494         $sql.= ' AND ( reservedate < NOW() )';
2495     }
2496     my $dbh = C4::Context->dbh();
2497     my @row = $dbh->selectrow_array(
2498         $sql,
2499         undef,
2500         $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2501     );
2502
2503     return @row ? $row[0]+1 : 1;
2504 }
2505
2506 =head2 IsItemOnHoldAndFound
2507
2508     my $bool = IsItemFoundHold( $itemnumber );
2509
2510     Returns true if the item is currently on hold
2511     and that hold has a non-null found status ( W, T, etc. )
2512
2513 =cut
2514
2515 sub IsItemOnHoldAndFound {
2516     my ($itemnumber) = @_;
2517
2518     my $rs = Koha::Database->new()->schema()->resultset('Reserve');
2519
2520     my $found = $rs->count(
2521         {
2522             itemnumber => $itemnumber,
2523             found      => { '!=' => undef }
2524         }
2525     );
2526
2527     return $found;
2528 }
2529
2530 =head1 AUTHOR
2531
2532 Koha Development Team <http://koha-community.org/>
2533
2534 =cut
2535
2536 1;