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