Bug 16011: $VERSION - Remove the $VERSION init
[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     # set the version for version checking
95     require Exporter;
96     @ISA = qw(Exporter);
97     @EXPORT = qw(
98         &AddReserve
99
100         &GetReserve
101         &GetReservesFromItemnumber
102         &GetReservesFromBiblionumber
103         &GetReservesFromBorrowernumber
104         &GetReservesForBranch
105         &GetReservesToBranch
106         &GetReserveCount
107         &GetReserveInfo
108         &GetReserveStatus
109
110         &GetOtherReserves
111
112         &ModReserveFill
113         &ModReserveAffect
114         &ModReserve
115         &ModReserveStatus
116         &ModReserveCancelAll
117         &ModReserveMinusPriority
118         &MoveReserve
119
120         &CheckReserves
121         &CanBookBeReserved
122         &CanItemBeReserved
123         &CanReserveBeCanceledFromOpac
124         &CancelReserve
125         &CancelExpiredReserves
126
127         &AutoUnsuspendReserves
128
129         &IsAvailableForItemLevelRequest
130
131         &OPACItemHoldsAllowed
132
133         &AlterPriority
134         &ToggleLowestPriority
135
136         &ReserveSlip
137         &ToggleSuspend
138         &SuspendAll
139
140         &GetReservesControlBranch
141
142         IsItemOnHoldAndFound
143     );
144     @EXPORT_OK = qw( MergeHolds );
145 }
146
147 =head2 AddReserve
148
149     AddReserve($branch,$borrowernumber,$biblionumber,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found)
150
151 Adds reserve and generates HOLDPLACED message.
152
153 The following tables are available witin the HOLDPLACED message:
154
155     branches
156     borrowers
157     biblio
158     biblioitems
159     items
160
161 =cut
162
163 sub AddReserve {
164     my (
165         $branch,    $borrowernumber, $biblionumber,
166         $bibitems,  $priority, $resdate, $expdate, $notes,
167         $title,      $checkitem, $found
168     ) = @_;
169
170     if ( Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->count() > 0 ) {
171         carp("AddReserve: borrower $borrowernumber already has a hold for biblionumber $biblionumber");
172         return;
173     }
174
175     my $dbh     = C4::Context->dbh;
176
177     $resdate = output_pref( { str => dt_from_string( $resdate ), dateonly => 1, dateformat => 'iso' })
178         or output_pref({ dt => dt_from_string, dateonly => 1, dateformat => 'iso' });
179
180     $expdate = output_pref({ str => $expdate, dateonly => 1, dateformat => 'iso' });
181
182     if ( C4::Context->preference('AllowHoldDateInFuture') ) {
183
184         # Make room in reserves for this before those of a later reserve date
185         $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
186     }
187
188     my $waitingdate;
189
190     # If the reserv had the waiting status, we had the value of the resdate
191     if ( $found eq 'W' ) {
192         $waitingdate = $resdate;
193     }
194
195     # updates take place here
196     my $hold = Koha::Hold->new(
197         {
198             borrowernumber => $borrowernumber,
199             biblionumber   => $biblionumber,
200             reservedate    => $resdate,
201             branchcode     => $branch,
202             priority       => $priority,
203             reservenotes   => $notes,
204             itemnumber     => $checkitem,
205             found          => $found,
206             waitingdate    => $waitingdate,
207             expirationdate => $expdate
208         }
209     )->store();
210     my $reserve_id = $hold->id();
211
212     # add a reserve fee if needed
213     my $fee = GetReserveFee( $borrowernumber, $biblionumber );
214     ChargeReserveFee( $borrowernumber, $fee, $title );
215
216     _FixPriority({ biblionumber => $biblionumber});
217
218     # Send e-mail to librarian if syspref is active
219     if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
220         my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber);
221         my $library = Koha::Libraries->find($borrower->{branchcode})->unblessed;
222         if ( my $letter =  C4::Letters::GetPreparedLetter (
223             module => 'reserves',
224             letter_code => 'HOLDPLACED',
225             branchcode => $branch,
226             tables => {
227                 'branches'    => $library,
228                 'borrowers'   => $borrower,
229                 'biblio'      => $biblionumber,
230                 'biblioitems' => $biblionumber,
231                 'items'       => $checkitem,
232             },
233         ) ) {
234
235             my $admin_email_address = $library->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress');
236
237             C4::Letters::EnqueueLetter(
238                 {   letter                 => $letter,
239                     borrowernumber         => $borrowernumber,
240                     message_transport_type => 'email',
241                     from_address           => $admin_email_address,
242                     to_address           => $admin_email_address,
243                 }
244             );
245         }
246     }
247
248     return $reserve_id;
249 }
250
251 =head2 GetReserve
252
253     $res = GetReserve( $reserve_id );
254
255     Return the current reserve.
256
257 =cut
258
259 sub GetReserve {
260     my ($reserve_id) = @_;
261
262     my $dbh = C4::Context->dbh;
263
264     my $query = "SELECT * FROM reserves WHERE reserve_id = ?";
265     my $sth = $dbh->prepare( $query );
266     $sth->execute( $reserve_id );
267     return $sth->fetchrow_hashref();
268 }
269
270 =head2 GetReservesFromBiblionumber
271
272   my $reserves = GetReservesFromBiblionumber({
273     biblionumber => $biblionumber,
274     [ itemnumber => $itemnumber, ]
275     [ all_dates => 1|0 ]
276   });
277
278 This function gets the list of reservations for one C<$biblionumber>,
279 returning an arrayref pointing to the reserves for C<$biblionumber>.
280
281 By default, only reserves whose start date falls before the current
282 time are returned.  To return all reserves, including future ones,
283 the C<all_dates> parameter can be included and set to a true value.
284
285 If the C<itemnumber> parameter is supplied, reserves must be targeted
286 to that item or not targeted to any item at all; otherwise, they
287 are excluded from the list.
288
289 =cut
290
291 sub GetReservesFromBiblionumber {
292     my ( $params ) = @_;
293     my $biblionumber = $params->{biblionumber} or return [];
294     my $itemnumber = $params->{itemnumber};
295     my $all_dates = $params->{all_dates} // 0;
296     my $dbh   = C4::Context->dbh;
297
298     # Find the desired items in the reserves
299     my @params;
300     my $query = "
301         SELECT  reserve_id,
302                 branchcode,
303                 timestamp AS rtimestamp,
304                 priority,
305                 biblionumber,
306                 borrowernumber,
307                 reservedate,
308                 found,
309                 itemnumber,
310                 reservenotes,
311                 expirationdate,
312                 lowestPriority,
313                 suspend,
314                 suspend_until
315         FROM     reserves
316         WHERE biblionumber = ? ";
317     push( @params, $biblionumber );
318     unless ( $all_dates ) {
319         $query .= " AND reservedate <= CAST(NOW() AS DATE) ";
320     }
321     if ( $itemnumber ) {
322         $query .= " AND ( itemnumber IS NULL OR itemnumber = ? )";
323         push( @params, $itemnumber );
324     }
325     $query .= "ORDER BY priority";
326     my $sth = $dbh->prepare($query);
327     $sth->execute( @params );
328     my @results;
329     while ( my $data = $sth->fetchrow_hashref ) {
330         push @results, $data;
331     }
332     return \@results;
333 }
334
335 =head2 GetReservesFromItemnumber
336
337  ( $reservedate, $borrowernumber, $branchcode, $reserve_id, $waitingdate ) = GetReservesFromItemnumber($itemnumber);
338
339 Get the first reserve for a specific item number (based on priority). Returns the abovementioned values for that reserve.
340
341 The routine does not look at future reserves (read: item level holds), but DOES include future waits (a confirmed future hold).
342
343 =cut
344
345 sub GetReservesFromItemnumber {
346     my ($itemnumber) = @_;
347
348     my $schema = Koha::Database->new()->schema();
349
350     my $r = $schema->resultset('Reserve')->search(
351         {
352             itemnumber => $itemnumber,
353             suspend    => 0,
354             -or        => [
355                 reservedate => \'<= CAST( NOW() AS DATE )',
356                 waitingdate => { '!=', undef }
357             ]
358         },
359         {
360             order_by => 'priority',
361         }
362     )->first();
363
364     return unless $r;
365
366     return (
367         $r->reservedate(),
368         $r->get_column('borrowernumber'),
369         $r->get_column('branchcode'),
370         $r->reserve_id(),
371         $r->waitingdate(),
372     );
373 }
374
375 =head2 GetReservesFromBorrowernumber
376
377     $borrowerreserv = GetReservesFromBorrowernumber($borrowernumber,$tatus);
378
379 TODO :: Descritpion
380
381 =cut
382
383 sub GetReservesFromBorrowernumber {
384     my ( $borrowernumber, $status ) = @_;
385     my $dbh   = C4::Context->dbh;
386     my $sth;
387     if ($status) {
388         $sth = $dbh->prepare("
389             SELECT *
390             FROM   reserves
391             WHERE  borrowernumber=?
392                 AND found =?
393             ORDER BY reservedate
394         ");
395         $sth->execute($borrowernumber,$status);
396     } else {
397         $sth = $dbh->prepare("
398             SELECT *
399             FROM   reserves
400             WHERE  borrowernumber=?
401             ORDER BY reservedate
402         ");
403         $sth->execute($borrowernumber);
404     }
405     my $data = $sth->fetchall_arrayref({});
406     return @$data;
407 }
408
409 =head2 CanBookBeReserved
410
411   $canReserve = &CanBookBeReserved($borrowernumber, $biblionumber)
412   if ($canReserve eq 'OK') { #We can reserve this Item! }
413
414 See CanItemBeReserved() for possible return values.
415
416 =cut
417
418 sub CanBookBeReserved{
419     my ($borrowernumber, $biblionumber) = @_;
420
421     my $items = GetItemnumbersForBiblio($biblionumber);
422     #get items linked via host records
423     my @hostitems = get_hostitemnumbers_of($biblionumber);
424     if (@hostitems){
425     push (@$items,@hostitems);
426     }
427
428     my $canReserve;
429     foreach my $item (@$items) {
430         $canReserve = CanItemBeReserved( $borrowernumber, $item );
431         return 'OK' if $canReserve eq 'OK';
432     }
433     return $canReserve;
434 }
435
436 =head2 CanItemBeReserved
437
438   $canReserve = &CanItemBeReserved($borrowernumber, $itemnumber)
439   if ($canReserve eq 'OK') { #We can reserve this Item! }
440
441 @RETURNS OK,              if the Item can be reserved.
442          ageRestricted,   if the Item is age restricted for this borrower.
443          damaged,         if the Item is damaged.
444          cannotReserveFromOtherBranches, if syspref 'canreservefromotherbranches' is OK.
445          tooManyReserves, if the borrower has exceeded his maximum reserve amount.
446          notReservable,   if holds on this item are not allowed
447
448 =cut
449
450 sub CanItemBeReserved{
451     my ($borrowernumber, $itemnumber) = @_;
452
453     my $dbh             = C4::Context->dbh;
454     my $ruleitemtype; # itemtype of the matching issuing rule
455     my $allowedreserves = 0;
456             
457     # we retrieve borrowers and items informations #
458     # item->{itype} will come for biblioitems if necessery
459     my $item = GetItem($itemnumber);
460     my $biblioData = C4::Biblio::GetBiblioData( $item->{biblionumber} );
461     my $borrower = C4::Members::GetMember('borrowernumber'=>$borrowernumber);
462
463     # If an item is damaged and we don't allow holds on damaged items, we can stop right here
464     return 'damaged' if ( $item->{damaged} && !C4::Context->preference('AllowHoldsOnDamagedItems') );
465
466     #Check for the age restriction
467     my ($ageRestriction, $daysToAgeRestriction) = C4::Circulation::GetAgeRestriction( $biblioData->{agerestriction}, $borrower );
468     return 'ageRestricted' if $daysToAgeRestriction && $daysToAgeRestriction > 0;
469
470     my $controlbranch = C4::Context->preference('ReservesControlBranch');
471
472     # we retrieve user rights on this itemtype and branchcode
473     my $sth = $dbh->prepare("SELECT categorycode, itemtype, branchcode, reservesallowed
474                              FROM issuingrules
475                              WHERE (categorycode in (?,'*') )
476                              AND (itemtype IN (?,'*'))
477                              AND (branchcode IN (?,'*'))
478                              ORDER BY
479                                categorycode DESC,
480                                itemtype     DESC,
481                                branchcode   DESC;"
482                            );
483
484     my $querycount ="SELECT
485                             count(*) as count
486                             FROM reserves
487                                 LEFT JOIN items USING (itemnumber)
488                                 LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
489                                 LEFT JOIN borrowers USING (borrowernumber)
490                             WHERE borrowernumber = ?
491                                 ";
492     
493     
494     my $branchcode   = "";
495     my $branchfield  = "reserves.branchcode";
496
497     if( $controlbranch eq "ItemHomeLibrary" ){
498         $branchfield = "items.homebranch";
499         $branchcode = $item->{homebranch};
500     }elsif( $controlbranch eq "PatronLibrary" ){
501         $branchfield = "borrowers.branchcode";
502         $branchcode = $borrower->{branchcode};
503     }
504     
505     # we retrieve rights 
506     $sth->execute($borrower->{'categorycode'}, $item->{'itype'}, $branchcode);
507     if(my $rights = $sth->fetchrow_hashref()){
508         $ruleitemtype    = $rights->{itemtype};
509         $allowedreserves = $rights->{reservesallowed}; 
510     }else{
511         $ruleitemtype = '*';
512     }
513
514     # we retrieve count
515
516     $querycount .= "AND $branchfield = ?";
517     
518     # If using item-level itypes, fall back to the record
519     # level itemtype if the hold has no associated item
520     $querycount .=
521       C4::Context->preference('item-level_itypes')
522       ? " AND COALESCE( itype, itemtype ) = ?"
523       : " AND itemtype = ?"
524       if ( $ruleitemtype ne "*" );
525
526     my $sthcount = $dbh->prepare($querycount);
527     
528     if($ruleitemtype eq "*"){
529         $sthcount->execute($borrowernumber, $branchcode);
530     }else{
531         $sthcount->execute($borrowernumber, $branchcode, $ruleitemtype);
532     }
533
534     my $reservecount = "0";
535     if(my $rowcount = $sthcount->fetchrow_hashref()){
536         $reservecount = $rowcount->{count};
537     }
538     # we check if it's ok or not
539     if( $reservecount >= $allowedreserves ){
540         return 'tooManyReserves';
541     }
542
543     my $circ_control_branch = C4::Circulation::_GetCircControlBranch($item,
544         $borrower);
545     my $branchitemrule = C4::Circulation::GetBranchItemRule($circ_control_branch,
546         $item->{itype});
547
548     if ( $branchitemrule->{holdallowed} == 0 ) {
549         return 'notReservable';
550     }
551
552     if (   $branchitemrule->{holdallowed} == 1
553         && $borrower->{branchcode} ne $item->{homebranch} )
554     {
555           return 'cannotReserveFromOtherBranches';
556     }
557
558     # If reservecount is ok, we check item branch if IndependentBranches is ON
559     # and canreservefromotherbranches is OFF
560     if ( C4::Context->preference('IndependentBranches')
561         and !C4::Context->preference('canreservefromotherbranches') )
562     {
563         my $itembranch = $item->{homebranch};
564         if ($itembranch ne $borrower->{branchcode}) {
565             return 'cannotReserveFromOtherBranches';
566         }
567     }
568
569     return 'OK';
570 }
571
572 =head2 CanReserveBeCanceledFromOpac
573
574     $number = CanReserveBeCanceledFromOpac($reserve_id, $borrowernumber);
575
576     returns 1 if reserve can be cancelled by user from OPAC.
577     First check if reserve belongs to user, next checks if reserve is not in
578     transfer or waiting status
579
580 =cut
581
582 sub CanReserveBeCanceledFromOpac {
583     my ($reserve_id, $borrowernumber) = @_;
584
585     return unless $reserve_id and $borrowernumber;
586     my $reserve = GetReserve($reserve_id);
587
588     return 0 unless $reserve->{borrowernumber} == $borrowernumber;
589     return 0 if ( $reserve->{found} eq 'W' ) or ( $reserve->{found} eq 'T' );
590
591     return 1;
592
593 }
594
595 =head2 GetReserveCount
596
597   $number = &GetReserveCount($borrowernumber);
598
599 this function returns the number of reservation for a borrower given on input arg.
600
601 =cut
602
603 sub GetReserveCount {
604     my ($borrowernumber) = @_;
605
606     my $dbh = C4::Context->dbh;
607
608     my $query = "
609         SELECT COUNT(*) AS counter
610         FROM reserves
611         WHERE borrowernumber = ?
612     ";
613     my $sth = $dbh->prepare($query);
614     $sth->execute($borrowernumber);
615     my $row = $sth->fetchrow_hashref;
616     return $row->{counter};
617 }
618
619 =head2 GetOtherReserves
620
621   ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber);
622
623 Check queued list of this document and check if this document must be transferred
624
625 =cut
626
627 sub GetOtherReserves {
628     my ($itemnumber) = @_;
629     my $messages;
630     my $nextreservinfo;
631     my ( undef, $checkreserves, undef ) = CheckReserves($itemnumber);
632     if ($checkreserves) {
633         my $iteminfo = GetItem($itemnumber);
634         if ( $iteminfo->{'holdingbranch'} ne $checkreserves->{'branchcode'} ) {
635             $messages->{'transfert'} = $checkreserves->{'branchcode'};
636             #minus priorities of others reservs
637             ModReserveMinusPriority(
638                 $itemnumber,
639                 $checkreserves->{'reserve_id'},
640             );
641
642             #launch the subroutine dotransfer
643             C4::Items::ModItemTransfer(
644                 $itemnumber,
645                 $iteminfo->{'holdingbranch'},
646                 $checkreserves->{'branchcode'}
647               ),
648               ;
649         }
650
651      #step 2b : case of a reservation on the same branch, set the waiting status
652         else {
653             $messages->{'waiting'} = 1;
654             ModReserveMinusPriority(
655                 $itemnumber,
656                 $checkreserves->{'reserve_id'},
657             );
658             ModReserveStatus($itemnumber,'W');
659         }
660
661         $nextreservinfo = $checkreserves->{'borrowernumber'};
662     }
663
664     return ( $messages, $nextreservinfo );
665 }
666
667 =head2 ChargeReserveFee
668
669     $fee = ChargeReserveFee( $borrowernumber, $fee, $title );
670
671     Charge the fee for a reserve (if $fee > 0)
672
673 =cut
674
675 sub ChargeReserveFee {
676     my ( $borrowernumber, $fee, $title ) = @_;
677     return if !$fee || $fee==0; # the last test is needed to include 0.00
678     my $accquery = qq{
679 INSERT INTO accountlines ( borrowernumber, accountno, date, amount, description, accounttype, amountoutstanding ) VALUES (?, ?, NOW(), ?, ?, 'Res', ?)
680     };
681     my $dbh = C4::Context->dbh;
682     my $nextacctno = &getnextacctno( $borrowernumber );
683     $dbh->do( $accquery, undef, ( $borrowernumber, $nextacctno, $fee, "Reserve Charge - $title", $fee ) );
684 }
685
686 =head2 GetReserveFee
687
688     $fee = GetReserveFee( $borrowernumber, $biblionumber );
689
690     Calculate the fee for a reserve (if applicable).
691
692 =cut
693
694 sub GetReserveFee {
695     my ( $borrowernumber, $biblionumber ) = @_;
696     my $borquery = qq{
697 SELECT reservefee FROM borrowers LEFT JOIN categories ON borrowers.categorycode = categories.categorycode WHERE borrowernumber = ?
698     };
699     my $issue_qry = qq{
700 SELECT COUNT(*) FROM items
701 LEFT JOIN issues USING (itemnumber)
702 WHERE items.biblionumber=? AND issues.issue_id IS NULL
703     };
704     my $holds_qry = qq{
705 SELECT COUNT(*) FROM reserves WHERE biblionumber=? AND borrowernumber<>?
706     };
707
708     my $dbh = C4::Context->dbh;
709     my ( $fee ) = $dbh->selectrow_array( $borquery, undef, ($borrowernumber) );
710     my $hold_fee_mode = C4::Context->preference('HoldFeeMode') || 'not_always';
711     if( $fee and $fee > 0 and $hold_fee_mode ne 'always' ) {
712         # This is a reconstruction of the old code:
713         # Compare number of items with items issued, and optionally check holds
714         # If not all items are issued and there are no holds: charge no fee
715         # NOTE: Lost, damaged, not-for-loan, etc. are just ignored here
716         my ( $notissued, $reserved );
717         ( $notissued ) = $dbh->selectrow_array( $issue_qry, undef,
718             ( $biblionumber ) );
719         if( $notissued ) {
720             ( $reserved ) = $dbh->selectrow_array( $holds_qry, undef,
721                 ( $biblionumber, $borrowernumber ) );
722             $fee = 0 if $reserved == 0;
723         }
724     }
725     return $fee;
726 }
727
728 =head2 GetReservesToBranch
729
730   @transreserv = GetReservesToBranch( $frombranch );
731
732 Get reserve list for a given branch
733
734 =cut
735
736 sub GetReservesToBranch {
737     my ( $frombranch ) = @_;
738     my $dbh = C4::Context->dbh;
739     my $sth = $dbh->prepare(
740         "SELECT reserve_id,borrowernumber,reservedate,itemnumber,timestamp
741          FROM reserves 
742          WHERE priority='0' 
743            AND branchcode=?"
744     );
745     $sth->execute( $frombranch );
746     my @transreserv;
747     my $i = 0;
748     while ( my $data = $sth->fetchrow_hashref ) {
749         $transreserv[$i] = $data;
750         $i++;
751     }
752     return (@transreserv);
753 }
754
755 =head2 GetReservesForBranch
756
757   @transreserv = GetReservesForBranch($frombranch);
758
759 =cut
760
761 sub GetReservesForBranch {
762     my ($frombranch) = @_;
763     my $dbh = C4::Context->dbh;
764
765     my $query = "
766         SELECT reserve_id,borrowernumber,reservedate,itemnumber,waitingdate
767         FROM   reserves 
768         WHERE   priority='0'
769         AND found='W'
770     ";
771     $query .= " AND branchcode=? " if ( $frombranch );
772     $query .= "ORDER BY waitingdate" ;
773
774     my $sth = $dbh->prepare($query);
775     if ($frombranch){
776      $sth->execute($frombranch);
777     } else {
778         $sth->execute();
779     }
780
781     my @transreserv;
782     my $i = 0;
783     while ( my $data = $sth->fetchrow_hashref ) {
784         $transreserv[$i] = $data;
785         $i++;
786     }
787     return (@transreserv);
788 }
789
790 =head2 GetReserveStatus
791
792   $reservestatus = GetReserveStatus($itemnumber);
793
794 Takes an itemnumber and returns the status of the reserve placed on it.
795 If several reserves exist, the reserve with the lower priority is given.
796
797 =cut
798
799 ## FIXME: I don't think this does what it thinks it does.
800 ## It only ever checks the first reserve result, even though
801 ## multiple reserves for that bib can have the itemnumber set
802 ## the sub is only used once in the codebase.
803 sub GetReserveStatus {
804     my ($itemnumber) = @_;
805
806     my $dbh = C4::Context->dbh;
807
808     my ($sth, $found, $priority);
809     if ( $itemnumber ) {
810         $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
811         $sth->execute($itemnumber);
812         ($found, $priority) = $sth->fetchrow_array;
813     }
814
815     if(defined $found) {
816         return 'Waiting'  if $found eq 'W' and $priority == 0;
817         return 'Finished' if $found eq 'F';
818     }
819
820     return 'Reserved' if $priority > 0;
821
822     return ''; # empty string here will remove need for checking undef, or less log lines
823 }
824
825 =head2 CheckReserves
826
827   ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber);
828   ($status, $reserve, $all_reserves) = &CheckReserves(undef, $barcode);
829   ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber,undef,$lookahead);
830
831 Find a book in the reserves.
832
833 C<$itemnumber> is the book's item number.
834 C<$lookahead> is the number of days to look in advance for future reserves.
835
836 As I understand it, C<&CheckReserves> looks for the given item in the
837 reserves. If it is found, that's a match, and C<$status> is set to
838 C<Waiting>.
839
840 Otherwise, it finds the most important item in the reserves with the
841 same biblio number as this book (I'm not clear on this) and returns it
842 with C<$status> set to C<Reserved>.
843
844 C<&CheckReserves> returns a two-element list:
845
846 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
847
848 C<$reserve> is the reserve item that matched. It is a
849 reference-to-hash whose keys are mostly the fields of the reserves
850 table in the Koha database.
851
852 =cut
853
854 sub CheckReserves {
855     my ( $item, $barcode, $lookahead_days, $ignore_borrowers) = @_;
856     my $dbh = C4::Context->dbh;
857     my $sth;
858     my $select;
859     if (C4::Context->preference('item-level_itypes')){
860         $select = "
861            SELECT items.biblionumber,
862            items.biblioitemnumber,
863            itemtypes.notforloan,
864            items.notforloan AS itemnotforloan,
865            items.itemnumber,
866            items.damaged,
867            items.homebranch,
868            items.holdingbranch
869            FROM   items
870            LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
871            LEFT JOIN itemtypes   ON items.itype   = itemtypes.itemtype
872         ";
873     }
874     else {
875         $select = "
876            SELECT items.biblionumber,
877            items.biblioitemnumber,
878            itemtypes.notforloan,
879            items.notforloan AS itemnotforloan,
880            items.itemnumber,
881            items.damaged,
882            items.homebranch,
883            items.holdingbranch
884            FROM   items
885            LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
886            LEFT JOIN itemtypes   ON biblioitems.itemtype   = itemtypes.itemtype
887         ";
888     }
889
890     if ($item) {
891         $sth = $dbh->prepare("$select WHERE itemnumber = ?");
892         $sth->execute($item);
893     }
894     else {
895         $sth = $dbh->prepare("$select WHERE barcode = ?");
896         $sth->execute($barcode);
897     }
898     # note: we get the itemnumber because we might have started w/ just the barcode.  Now we know for sure we have it.
899     my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber, $damaged, $item_homebranch, $item_holdingbranch ) = $sth->fetchrow_array;
900
901     return if ( $damaged && !C4::Context->preference('AllowHoldsOnDamagedItems') );
902
903     return unless $itemnumber; # bail if we got nothing.
904
905     # if item is not for loan it cannot be reserved either.....
906     # except where items.notforloan < 0 :  This indicates the item is holdable.
907     return if  ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
908
909     # Find this item in the reserves
910     my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber, $lookahead_days, $ignore_borrowers);
911
912     # $priority and $highest are used to find the most important item
913     # in the list returned by &_Findgroupreserve. (The lower $priority,
914     # the more important the item.)
915     # $highest is the most important item we've seen so far.
916     my $highest;
917     if (scalar @reserves) {
918         my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority');
919         my $LocalHoldsPriorityPatronControl = C4::Context->preference('LocalHoldsPriorityPatronControl');
920         my $LocalHoldsPriorityItemControl = C4::Context->preference('LocalHoldsPriorityItemControl');
921
922         my $priority = 10000000;
923         foreach my $res (@reserves) {
924             if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
925                 return ( "Waiting", $res, \@reserves ); # Found it
926             } else {
927                 my $borrowerinfo;
928                 my $iteminfo;
929                 my $local_hold_match;
930
931                 if ($LocalHoldsPriority) {
932                     $borrowerinfo = C4::Members::GetMember( borrowernumber => $res->{'borrowernumber'} );
933                     $iteminfo = C4::Items::GetItem($itemnumber);
934
935                     my $local_holds_priority_item_branchcode =
936                       $iteminfo->{$LocalHoldsPriorityItemControl};
937                     my $local_holds_priority_patron_branchcode =
938                       ( $LocalHoldsPriorityPatronControl eq 'PickupLibrary' )
939                       ? $res->{branchcode}
940                       : ( $LocalHoldsPriorityPatronControl eq 'HomeLibrary' )
941                       ? $borrowerinfo->{branchcode}
942                       : undef;
943                     $local_hold_match =
944                       $local_holds_priority_item_branchcode eq
945                       $local_holds_priority_patron_branchcode;
946                 }
947
948                 # See if this item is more important than what we've got so far
949                 if ( ( $res->{'priority'} && $res->{'priority'} < $priority ) || $local_hold_match ) {
950                     $borrowerinfo ||= C4::Members::GetMember( borrowernumber => $res->{'borrowernumber'} );
951                     $iteminfo ||= C4::Items::GetItem($itemnumber);
952                     my $branch = GetReservesControlBranch( $iteminfo, $borrowerinfo );
953                     my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$iteminfo->{'itype'});
954                     next if ($branchitemrule->{'holdallowed'} == 0);
955                     next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $borrowerinfo->{'branchcode'}));
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;