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