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