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