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