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