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