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