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