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