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