Bug 20912: (QA follow-up) Move Fees to Charges::
[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     return if !$fee || $fee == 0;    # the last test is needed to include 0.00
574     Koha::Account->new( { patron_id => $borrowernumber } )->add_debit(
575         {
576             amount       => $fee,
577             description  => "Reserve Charge - " . $title,
578             note         => undef,
579             user_id      => C4::Context->userenv ? C4::Context->userenv->{'number'} : 0,
580             library_id   => C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef,
581             sip          => undef,
582             invoice_type => undef,
583             type         => 'reserve',
584             item_id      => undef
585         }
586     );
587 }
588
589 =head2 GetReserveFee
590
591     $fee = GetReserveFee( $borrowernumber, $biblionumber );
592
593     Calculate the fee for a reserve (if applicable).
594
595 =cut
596
597 sub GetReserveFee {
598     my ( $borrowernumber, $biblionumber ) = @_;
599     my $borquery = qq{
600 SELECT reservefee FROM borrowers LEFT JOIN categories ON borrowers.categorycode = categories.categorycode WHERE borrowernumber = ?
601     };
602     my $issue_qry = qq{
603 SELECT COUNT(*) FROM items
604 LEFT JOIN issues USING (itemnumber)
605 WHERE items.biblionumber=? AND issues.issue_id IS NULL
606     };
607     my $holds_qry = qq{
608 SELECT COUNT(*) FROM reserves WHERE biblionumber=? AND borrowernumber<>?
609     };
610
611     my $dbh = C4::Context->dbh;
612     my ( $fee ) = $dbh->selectrow_array( $borquery, undef, ($borrowernumber) );
613     my $hold_fee_mode = C4::Context->preference('HoldFeeMode') || 'not_always';
614     if( $fee and $fee > 0 and $hold_fee_mode eq 'not_always' ) {
615         # This is a reconstruction of the old code:
616         # Compare number of items with items issued, and optionally check holds
617         # If not all items are issued and there are no holds: charge no fee
618         # NOTE: Lost, damaged, not-for-loan, etc. are just ignored here
619         my ( $notissued, $reserved );
620         ( $notissued ) = $dbh->selectrow_array( $issue_qry, undef,
621             ( $biblionumber ) );
622         if( $notissued ) {
623             ( $reserved ) = $dbh->selectrow_array( $holds_qry, undef,
624                 ( $biblionumber, $borrowernumber ) );
625             $fee = 0 if $reserved == 0;
626         }
627     }
628     return $fee;
629 }
630
631 =head2 GetReserveStatus
632
633   $reservestatus = GetReserveStatus($itemnumber);
634
635 Takes an itemnumber and returns the status of the reserve placed on it.
636 If several reserves exist, the reserve with the lower priority is given.
637
638 =cut
639
640 ## FIXME: I don't think this does what it thinks it does.
641 ## It only ever checks the first reserve result, even though
642 ## multiple reserves for that bib can have the itemnumber set
643 ## the sub is only used once in the codebase.
644 sub GetReserveStatus {
645     my ($itemnumber) = @_;
646
647     my $dbh = C4::Context->dbh;
648
649     my ($sth, $found, $priority);
650     if ( $itemnumber ) {
651         $sth = $dbh->prepare("SELECT found, priority FROM reserves WHERE itemnumber = ? order by priority LIMIT 1");
652         $sth->execute($itemnumber);
653         ($found, $priority) = $sth->fetchrow_array;
654     }
655
656     if(defined $found) {
657         return 'Waiting'  if $found eq 'W' and $priority == 0;
658         return 'Finished' if $found eq 'F';
659     }
660
661     return 'Reserved' if $priority > 0;
662
663     return ''; # empty string here will remove need for checking undef, or less log lines
664 }
665
666 =head2 CheckReserves
667
668   ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber);
669   ($status, $reserve, $all_reserves) = &CheckReserves(undef, $barcode);
670   ($status, $reserve, $all_reserves) = &CheckReserves($itemnumber,undef,$lookahead);
671
672 Find a book in the reserves.
673
674 C<$itemnumber> is the book's item number.
675 C<$lookahead> is the number of days to look in advance for future reserves.
676
677 As I understand it, C<&CheckReserves> looks for the given item in the
678 reserves. If it is found, that's a match, and C<$status> is set to
679 C<Waiting>.
680
681 Otherwise, it finds the most important item in the reserves with the
682 same biblio number as this book (I'm not clear on this) and returns it
683 with C<$status> set to C<Reserved>.
684
685 C<&CheckReserves> returns a two-element list:
686
687 C<$status> is either C<Waiting>, C<Reserved> (see above), or 0.
688
689 C<$reserve> is the reserve item that matched. It is a
690 reference-to-hash whose keys are mostly the fields of the reserves
691 table in the Koha database.
692
693 =cut
694
695 sub CheckReserves {
696     my ( $item, $barcode, $lookahead_days, $ignore_borrowers) = @_;
697     my $dbh = C4::Context->dbh;
698     my $sth;
699     my $select;
700     if (C4::Context->preference('item-level_itypes')){
701         $select = "
702            SELECT items.biblionumber,
703            items.biblioitemnumber,
704            itemtypes.notforloan,
705            items.notforloan AS itemnotforloan,
706            items.itemnumber,
707            items.damaged,
708            items.homebranch,
709            items.holdingbranch
710            FROM   items
711            LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
712            LEFT JOIN itemtypes   ON items.itype   = itemtypes.itemtype
713         ";
714     }
715     else {
716         $select = "
717            SELECT items.biblionumber,
718            items.biblioitemnumber,
719            itemtypes.notforloan,
720            items.notforloan AS itemnotforloan,
721            items.itemnumber,
722            items.damaged,
723            items.homebranch,
724            items.holdingbranch
725            FROM   items
726            LEFT JOIN biblioitems ON items.biblioitemnumber = biblioitems.biblioitemnumber
727            LEFT JOIN itemtypes   ON biblioitems.itemtype   = itemtypes.itemtype
728         ";
729     }
730
731     if ($item) {
732         $sth = $dbh->prepare("$select WHERE itemnumber = ?");
733         $sth->execute($item);
734     }
735     else {
736         $sth = $dbh->prepare("$select WHERE barcode = ?");
737         $sth->execute($barcode);
738     }
739     # note: we get the itemnumber because we might have started w/ just the barcode.  Now we know for sure we have it.
740     my ( $biblio, $bibitem, $notforloan_per_itemtype, $notforloan_per_item, $itemnumber, $damaged, $item_homebranch, $item_holdingbranch ) = $sth->fetchrow_array;
741
742     return if ( $damaged && !C4::Context->preference('AllowHoldsOnDamagedItems') );
743
744     return unless $itemnumber; # bail if we got nothing.
745
746     # if item is not for loan it cannot be reserved either.....
747     # except where items.notforloan < 0 :  This indicates the item is holdable.
748     return if  ( $notforloan_per_item > 0 ) or $notforloan_per_itemtype;
749
750     # Find this item in the reserves
751     my @reserves = _Findgroupreserve( $bibitem, $biblio, $itemnumber, $lookahead_days, $ignore_borrowers);
752
753     # $priority and $highest are used to find the most important item
754     # in the list returned by &_Findgroupreserve. (The lower $priority,
755     # the more important the item.)
756     # $highest is the most important item we've seen so far.
757     my $highest;
758     if (scalar @reserves) {
759         my $LocalHoldsPriority = C4::Context->preference('LocalHoldsPriority');
760         my $LocalHoldsPriorityPatronControl = C4::Context->preference('LocalHoldsPriorityPatronControl');
761         my $LocalHoldsPriorityItemControl = C4::Context->preference('LocalHoldsPriorityItemControl');
762
763         my $priority = 10000000;
764         foreach my $res (@reserves) {
765             if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) {
766                 if ($res->{'found'} eq 'W') {
767                     return ( "Waiting", $res, \@reserves ); # Found it, it is waiting
768                 } else {
769                     return ( "Reserved", $res, \@reserves ); # Found determinated hold, e. g. the tranferred one
770                 }
771             } else {
772                 my $patron;
773                 my $item;
774                 my $local_hold_match;
775
776                 if ($LocalHoldsPriority) {
777                     $patron = Koha::Patrons->find( $res->{borrowernumber} );
778                     $item = Koha::Items->find($itemnumber);
779
780                     my $local_holds_priority_item_branchcode =
781                       $item->$LocalHoldsPriorityItemControl;
782                     my $local_holds_priority_patron_branchcode =
783                       ( $LocalHoldsPriorityPatronControl eq 'PickupLibrary' )
784                       ? $res->{branchcode}
785                       : ( $LocalHoldsPriorityPatronControl eq 'HomeLibrary' )
786                       ? $patron->branchcode
787                       : undef;
788                     $local_hold_match =
789                       $local_holds_priority_item_branchcode eq
790                       $local_holds_priority_patron_branchcode;
791                 }
792
793                 # See if this item is more important than what we've got so far
794                 if ( ( $res->{'priority'} && $res->{'priority'} < $priority ) || $local_hold_match ) {
795                     $item ||= Koha::Items->find($itemnumber);
796                     next if $res->{itemtype} && $res->{itemtype} ne $item->effective_itemtype;
797                     $patron ||= Koha::Patrons->find( $res->{borrowernumber} );
798                     my $branch = GetReservesControlBranch( $item->unblessed, $patron->unblessed );
799                     my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$item->effective_itemtype);
800                     next if ($branchitemrule->{'holdallowed'} == 0);
801                     next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $patron->branchcode));
802                     my $hold_fulfillment_policy = $branchitemrule->{hold_fulfillment_policy};
803                     next if ( ($branchitemrule->{hold_fulfillment_policy} ne 'any') && ($res->{branchcode} ne $item->$hold_fulfillment_policy) );
804                     $priority = $res->{'priority'};
805                     $highest  = $res;
806                     last if $local_hold_match;
807                 }
808             }
809         }
810     }
811
812     # If we get this far, then no exact match was found.
813     # We return the most important (i.e. next) reservation.
814     if ($highest) {
815         $highest->{'itemnumber'} = $item;
816         return ( "Reserved", $highest, \@reserves );
817     }
818
819     return ( '' );
820 }
821
822 =head2 CancelExpiredReserves
823
824   CancelExpiredReserves();
825
826 Cancels all reserves with an expiration date from before today.
827
828 =cut
829
830 sub CancelExpiredReserves {
831     my $today = dt_from_string();
832     my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
833     my $expireWaiting = C4::Context->preference('ExpireReservesMaxPickUpDelay');
834
835     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
836     my $params = { expirationdate => { '<', $dtf->format_date($today) } };
837     $params->{found} = undef unless $expireWaiting;
838
839     # FIXME To move to Koha::Holds->search_expired (?)
840     my $holds = Koha::Holds->search( $params );
841
842     while ( my $hold = $holds->next ) {
843         my $calendar = Koha::Calendar->new( branchcode => $hold->branchcode );
844
845         next if !$cancel_on_holidays && $calendar->is_holiday( $today );
846
847         my $cancel_params = {};
848         if ( $hold->found eq 'W' ) {
849             $cancel_params->{charge_cancel_fee} = 1;
850         }
851         $hold->cancel( $cancel_params );
852     }
853 }
854
855 =head2 AutoUnsuspendReserves
856
857   AutoUnsuspendReserves();
858
859 Unsuspends all suspended reserves with a suspend_until date from before today.
860
861 =cut
862
863 sub AutoUnsuspendReserves {
864     my $today = dt_from_string();
865
866     my @holds = Koha::Holds->search( { suspend_until => { '<=' => $today->ymd() } } );
867
868     map { $_->resume() } @holds;
869 }
870
871 =head2 ModReserve
872
873   ModReserve({ rank => $rank,
874                reserve_id => $reserve_id,
875                branchcode => $branchcode
876                [, itemnumber => $itemnumber ]
877                [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
878               });
879
880 Change a hold request's priority or cancel it.
881
882 C<$rank> specifies the effect of the change.  If C<$rank>
883 is 'W' or 'n', nothing happens.  This corresponds to leaving a
884 request alone when changing its priority in the holds queue
885 for a bib.
886
887 If C<$rank> is 'del', the hold request is cancelled.
888
889 If C<$rank> is an integer greater than zero, the priority of
890 the request is set to that value.  Since priority != 0 means
891 that the item is not waiting on the hold shelf, setting the
892 priority to a non-zero value also sets the request's found
893 status and waiting date to NULL.
894
895 The optional C<$itemnumber> parameter is used only when
896 C<$rank> is a non-zero integer; if supplied, the itemnumber
897 of the hold request is set accordingly; if omitted, the itemnumber
898 is cleared.
899
900 B<FIXME:> Note that the forgoing can have the effect of causing
901 item-level hold requests to turn into title-level requests.  This
902 will be fixed once reserves has separate columns for requested
903 itemnumber and supplying itemnumber.
904
905 =cut
906
907 sub ModReserve {
908     my ( $params ) = @_;
909
910     my $rank = $params->{'rank'};
911     my $reserve_id = $params->{'reserve_id'};
912     my $branchcode = $params->{'branchcode'};
913     my $itemnumber = $params->{'itemnumber'};
914     my $suspend_until = $params->{'suspend_until'};
915     my $borrowernumber = $params->{'borrowernumber'};
916     my $biblionumber = $params->{'biblionumber'};
917
918     return if $rank eq "W";
919     return if $rank eq "n";
920
921     return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
922
923     my $hold;
924     unless ( $reserve_id ) {
925         my $holds = Koha::Holds->search({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber });
926         return unless $holds->count; # FIXME Should raise an exception
927         $hold = $holds->next;
928         $reserve_id = $hold->reserve_id;
929     }
930
931     $hold ||= Koha::Holds->find($reserve_id);
932
933     if ( $rank eq "del" ) {
934         $hold->cancel;
935     }
936     elsif ($rank =~ /^\d+/ and $rank > 0) {
937         logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->unblessed) )
938             if C4::Context->preference('HoldsLog');
939
940         $hold->set(
941             {
942                 priority    => $rank,
943                 branchcode  => $branchcode,
944                 itemnumber  => $itemnumber,
945                 found       => undef,
946                 waitingdate => undef
947             }
948         )->store();
949
950         if ( defined( $suspend_until ) ) {
951             if ( $suspend_until ) {
952                 $suspend_until = eval { dt_from_string( $suspend_until ) };
953                 $hold->suspend_hold( $suspend_until );
954             } else {
955                 # If the hold is suspended leave the hold suspended, but convert it to an indefinite hold.
956                 # If the hold is not suspended, this does nothing.
957                 $hold->set( { suspend_until => undef } )->store();
958             }
959         }
960
961         _FixPriority({ reserve_id => $reserve_id, rank =>$rank });
962     }
963 }
964
965 =head2 ModReserveFill
966
967   &ModReserveFill($reserve);
968
969 Fill a reserve. If I understand this correctly, this means that the
970 reserved book has been found and given to the patron who reserved it.
971
972 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
973 whose keys are fields from the reserves table in the Koha database.
974
975 =cut
976
977 sub ModReserveFill {
978     my ($res) = @_;
979     my $reserve_id = $res->{'reserve_id'};
980
981     my $hold = Koha::Holds->find($reserve_id);
982
983     # get the priority on this record....
984     my $priority = $hold->priority;
985
986     # update the hold statuses, no need to store it though, we will be deleting it anyway
987     $hold->set(
988         {
989             found    => 'F',
990             priority => 0,
991         }
992     );
993
994     # FIXME Must call Koha::Hold->cancel ? => No, should call ->filled and add the correct log
995     Koha::Old::Hold->new( $hold->unblessed() )->store();
996
997     $hold->delete();
998
999     if ( C4::Context->preference('HoldFeeMode') eq 'any_time_is_collected' ) {
1000         my $reserve_fee = GetReserveFee( $hold->borrowernumber, $hold->biblionumber );
1001         ChargeReserveFee( $hold->borrowernumber, $reserve_fee, $hold->biblio->title );
1002     }
1003
1004     # now fix the priority on the others (if the priority wasn't
1005     # already sorted!)....
1006     unless ( $priority == 0 ) {
1007         _FixPriority( { reserve_id => $reserve_id, biblionumber => $hold->biblionumber } );
1008     }
1009 }
1010
1011 =head2 ModReserveStatus
1012
1013   &ModReserveStatus($itemnumber, $newstatus);
1014
1015 Update the reserve status for the active (priority=0) reserve.
1016
1017 $itemnumber is the itemnumber the reserve is on
1018
1019 $newstatus is the new status.
1020
1021 =cut
1022
1023 sub ModReserveStatus {
1024
1025     #first : check if we have a reservation for this item .
1026     my ($itemnumber, $newstatus) = @_;
1027     my $dbh = C4::Context->dbh;
1028
1029     my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1030     my $sth_set = $dbh->prepare($query);
1031     $sth_set->execute( $newstatus, $itemnumber );
1032
1033     if ( C4::Context->preference("ReturnToShelvingCart") && $newstatus ) {
1034       CartToShelf( $itemnumber );
1035     }
1036 }
1037
1038 =head2 ModReserveAffect
1039
1040   &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend,$reserve_id);
1041
1042 This function affect an item and a status for a given reserve, either fetched directly
1043 by record_id, or by borrowernumber and itemnumber or biblionumber. If only biblionumber
1044 is given, only first reserve returned is affected, which is ok for anything but
1045 multi-item holds.
1046
1047 if $transferToDo is not set, then the status is set to "Waiting" as well.
1048 otherwise, a transfer is on the way, and the end of the transfer will
1049 take care of the waiting status
1050
1051 =cut
1052
1053 sub ModReserveAffect {
1054     my ( $itemnumber, $borrowernumber, $transferToDo, $reserve_id ) = @_;
1055     my $dbh = C4::Context->dbh;
1056
1057     # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1058     # attached to $itemnumber
1059     my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1060     $sth->execute($itemnumber);
1061     my ($biblionumber) = $sth->fetchrow;
1062
1063     # get request - need to find out if item is already
1064     # waiting in order to not send duplicate hold filled notifications
1065
1066     my $hold;
1067     # Find hold by id if we have it
1068     $hold = Koha::Holds->find( $reserve_id ) if $reserve_id;
1069     # Find item level hold for this item if there is one
1070     $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->next();
1071     # Find record level hold if there is no item level hold
1072     $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->next();
1073
1074     return unless $hold;
1075
1076     my $already_on_shelf = $hold->found && $hold->found eq 'W';
1077
1078     $hold->itemnumber($itemnumber);
1079     $hold->set_waiting($transferToDo);
1080
1081     _koha_notify_reserve( $hold->reserve_id )
1082       if ( !$transferToDo && !$already_on_shelf );
1083
1084     _FixPriority( { biblionumber => $biblionumber } );
1085
1086     if ( C4::Context->preference("ReturnToShelvingCart") ) {
1087         CartToShelf($itemnumber);
1088     }
1089
1090     return;
1091 }
1092
1093 =head2 ModReserveCancelAll
1094
1095   ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1096
1097 function to cancel reserv,check other reserves, and transfer document if it's necessary
1098
1099 =cut
1100
1101 sub ModReserveCancelAll {
1102     my $messages;
1103     my $nextreservinfo;
1104     my ( $itemnumber, $borrowernumber ) = @_;
1105
1106     #step 1 : cancel the reservation
1107     my $holds = Koha::Holds->search({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1108     return unless $holds->count;
1109     $holds->next->cancel;
1110
1111     #step 2 launch the subroutine of the others reserves
1112     ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1113
1114     return ( $messages, $nextreservinfo );
1115 }
1116
1117 =head2 ModReserveMinusPriority
1118
1119   &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1120
1121 Reduce the values of queued list
1122
1123 =cut
1124
1125 sub ModReserveMinusPriority {
1126     my ( $itemnumber, $reserve_id ) = @_;
1127
1128     #first step update the value of the first person on reserv
1129     my $dbh   = C4::Context->dbh;
1130     my $query = "
1131         UPDATE reserves
1132         SET    priority = 0 , itemnumber = ?
1133         WHERE  reserve_id = ?
1134     ";
1135     my $sth_upd = $dbh->prepare($query);
1136     $sth_upd->execute( $itemnumber, $reserve_id );
1137     # second step update all others reserves
1138     _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1139 }
1140
1141 =head2 IsAvailableForItemLevelRequest
1142
1143   my $is_available = IsAvailableForItemLevelRequest($item_record,$borrower_record);
1144
1145 Checks whether a given item record is available for an
1146 item-level hold request.  An item is available if
1147
1148 * it is not lost AND
1149 * it is not damaged AND
1150 * it is not withdrawn AND
1151 * a waiting or in transit reserve is placed on
1152 * does not have a not for loan value > 0
1153
1154 Need to check the issuingrules onshelfholds column,
1155 if this is set items on the shelf can be placed on hold
1156
1157 Note that IsAvailableForItemLevelRequest() does not
1158 check if the staff operator is authorized to place
1159 a request on the item - in particular,
1160 this routine does not check IndependentBranches
1161 and canreservefromotherbranches.
1162
1163 =cut
1164
1165 sub IsAvailableForItemLevelRequest {
1166     my $item = shift;
1167     my $borrower = shift;
1168
1169     my $dbh = C4::Context->dbh;
1170     # must check the notforloan setting of the itemtype
1171     # FIXME - a lot of places in the code do this
1172     #         or something similar - need to be
1173     #         consolidated
1174     my $patron = Koha::Patrons->find( $borrower->{borrowernumber} );
1175     my $item_object = Koha::Items->find( $item->{itemnumber } );
1176     my $itemtype = $item_object->effective_itemtype;
1177     my $notforloan_per_itemtype
1178       = $dbh->selectrow_array("SELECT notforloan FROM itemtypes WHERE itemtype = ?",
1179                               undef, $itemtype);
1180
1181     return 0 if
1182         $notforloan_per_itemtype ||
1183         $item->{itemlost}        ||
1184         $item->{notforloan} > 0  ||
1185         $item->{withdrawn}        ||
1186         ($item->{damaged} && !C4::Context->preference('AllowHoldsOnDamagedItems'));
1187
1188     my $on_shelf_holds = Koha::IssuingRules->get_onshelfholds_policy( { item => $item_object, patron => $patron } );
1189
1190     if ( $on_shelf_holds == 1 ) {
1191         return 1;
1192     } elsif ( $on_shelf_holds == 2 ) {
1193         my @items =
1194           Koha::Items->search( { biblionumber => $item->{biblionumber} } );
1195
1196         my $any_available = 0;
1197
1198         foreach my $i (@items) {
1199
1200             my $circ_control_branch = C4::Circulation::_GetCircControlBranch( $i->unblessed(), $borrower );
1201             my $branchitemrule = C4::Circulation::GetBranchItemRule( $circ_control_branch, $i->itype );
1202
1203             $any_available = 1
1204               unless $i->itemlost
1205               || $i->notforloan > 0
1206               || $i->withdrawn
1207               || $i->onloan
1208               || IsItemOnHoldAndFound( $i->id )
1209               || ( $i->damaged
1210                 && !C4::Context->preference('AllowHoldsOnDamagedItems') )
1211               || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan
1212               || $branchitemrule->{holdallowed} == 1 && $borrower->{branchcode} ne $i->homebranch;
1213         }
1214
1215         return $any_available ? 0 : 1;
1216     } else { # on_shelf_holds == 0 "If any unavailable" (the description is rather cryptic and could still be improved)
1217         return $item->{onloan} || IsItemOnHoldAndFound( $item->{itemnumber} );
1218     }
1219 }
1220
1221 sub _get_itype {
1222     my $item = shift;
1223
1224     my $itype;
1225     if (C4::Context->preference('item-level_itypes')) {
1226         # We can't trust GetItem to honour the syspref, so safest to do it ourselves
1227         # When GetItem is fixed, we can remove this
1228         $itype = $item->{itype};
1229     }
1230     else {
1231         # XXX This is a bit dodgy. It relies on biblio itemtype column having different name.
1232         # So if we already have a biblioitems join when calling this function,
1233         # we don't need to access the database again
1234         $itype = $item->{itemtype};
1235     }
1236     unless ($itype) {
1237         my $dbh = C4::Context->dbh;
1238         my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? ";
1239         my $sth = $dbh->prepare($query);
1240         $sth->execute($item->{biblioitemnumber});
1241         if (my $data = $sth->fetchrow_hashref()){
1242             $itype = $data->{itemtype};
1243         }
1244     }
1245     return $itype;
1246 }
1247
1248 =head2 AlterPriority
1249
1250   AlterPriority( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority );
1251
1252 This function changes a reserve's priority up, down, to the top, or to the bottom.
1253 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1254
1255 =cut
1256
1257 sub AlterPriority {
1258     my ( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority ) = @_;
1259
1260     my $hold = Koha::Holds->find( $reserve_id );
1261     return unless $hold;
1262
1263     if ( $hold->cancellationdate ) {
1264         warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (" . $hold->cancellationdate . ')';
1265         return;
1266     }
1267
1268     if ( $where eq 'up' ) {
1269       return unless $prev_priority;
1270       _FixPriority({ reserve_id => $reserve_id, rank => $prev_priority })
1271     } elsif ( $where eq 'down' ) {
1272       return unless $next_priority;
1273       _FixPriority({ reserve_id => $reserve_id, rank => $next_priority })
1274     } elsif ( $where eq 'top' ) {
1275       _FixPriority({ reserve_id => $reserve_id, rank => $first_priority })
1276     } elsif ( $where eq 'bottom' ) {
1277       _FixPriority({ reserve_id => $reserve_id, rank => $last_priority });
1278     }
1279
1280     # FIXME Should return the new priority
1281 }
1282
1283 =head2 ToggleLowestPriority
1284
1285   ToggleLowestPriority( $borrowernumber, $biblionumber );
1286
1287 This function sets the lowestPriority field to true if is false, and false if it is true.
1288
1289 =cut
1290
1291 sub ToggleLowestPriority {
1292     my ( $reserve_id ) = @_;
1293
1294     my $dbh = C4::Context->dbh;
1295
1296     my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1297     $sth->execute( $reserve_id );
1298
1299     _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1300 }
1301
1302 =head2 ToggleSuspend
1303
1304   ToggleSuspend( $reserve_id );
1305
1306 This function sets the suspend field to true if is false, and false if it is true.
1307 If the reserve is currently suspended with a suspend_until date, that date will
1308 be cleared when it is unsuspended.
1309
1310 =cut
1311
1312 sub ToggleSuspend {
1313     my ( $reserve_id, $suspend_until ) = @_;
1314
1315     $suspend_until = dt_from_string($suspend_until) if ($suspend_until);
1316
1317     my $hold = Koha::Holds->find( $reserve_id );
1318
1319     if ( $hold->is_suspended ) {
1320         $hold->resume()
1321     } else {
1322         $hold->suspend_hold( $suspend_until );
1323     }
1324 }
1325
1326 =head2 SuspendAll
1327
1328   SuspendAll(
1329       borrowernumber   => $borrowernumber,
1330       [ biblionumber   => $biblionumber, ]
1331       [ suspend_until  => $suspend_until, ]
1332       [ suspend        => $suspend ]
1333   );
1334
1335   This function accepts a set of hash keys as its parameters.
1336   It requires either borrowernumber or biblionumber, or both.
1337
1338   suspend_until is wholly optional.
1339
1340 =cut
1341
1342 sub SuspendAll {
1343     my %params = @_;
1344
1345     my $borrowernumber = $params{'borrowernumber'} || undef;
1346     my $biblionumber   = $params{'biblionumber'}   || undef;
1347     my $suspend_until  = $params{'suspend_until'}  || undef;
1348     my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1349
1350     $suspend_until = eval { dt_from_string($suspend_until) }
1351       if ( defined($suspend_until) );
1352
1353     return unless ( $borrowernumber || $biblionumber );
1354
1355     my $params;
1356     $params->{found}          = undef;
1357     $params->{borrowernumber} = $borrowernumber if $borrowernumber;
1358     $params->{biblionumber}   = $biblionumber if $biblionumber;
1359
1360     my @holds = Koha::Holds->search($params);
1361
1362     if ($suspend) {
1363         map { $_->suspend_hold($suspend_until) } @holds;
1364     }
1365     else {
1366         map { $_->resume() } @holds;
1367     }
1368 }
1369
1370
1371 =head2 _FixPriority
1372
1373   _FixPriority({
1374     reserve_id => $reserve_id,
1375     [rank => $rank,]
1376     [ignoreSetLowestRank => $ignoreSetLowestRank]
1377   });
1378
1379   or
1380
1381   _FixPriority({ biblionumber => $biblionumber});
1382
1383 This routine adjusts the priority of a hold request and holds
1384 on the same bib.
1385
1386 In the first form, where a reserve_id is passed, the priority of the
1387 hold is set to supplied rank, and other holds for that bib are adjusted
1388 accordingly.  If the rank is "del", the hold is cancelled.  If no rank
1389 is supplied, all of the holds on that bib have their priority adjusted
1390 as if the second form had been used.
1391
1392 In the second form, where a biblionumber is passed, the holds on that
1393 bib (that are not captured) are sorted in order of increasing priority,
1394 then have reserves.priority set so that the first non-captured hold
1395 has its priority set to 1, the second non-captured hold has its priority
1396 set to 2, and so forth.
1397
1398 In both cases, holds that have the lowestPriority flag on are have their
1399 priority adjusted to ensure that they remain at the end of the line.
1400
1401 Note that the ignoreSetLowestRank parameter is meant to be used only
1402 when _FixPriority calls itself.
1403
1404 =cut
1405
1406 sub _FixPriority {
1407     my ( $params ) = @_;
1408     my $reserve_id = $params->{reserve_id};
1409     my $rank = $params->{rank} // '';
1410     my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1411     my $biblionumber = $params->{biblionumber};
1412
1413     my $dbh = C4::Context->dbh;
1414
1415     my $hold;
1416     if ( $reserve_id ) {
1417         $hold = Koha::Holds->find( $reserve_id );
1418         return unless $hold;
1419     }
1420
1421     unless ( $biblionumber ) { # FIXME This is a very weird API
1422         $biblionumber = $hold->biblionumber;
1423     }
1424
1425     if ( $rank eq "del" ) { # FIXME will crash if called without $hold
1426         $hold->cancel;
1427     }
1428     elsif ( $rank eq "W" || $rank eq "0" ) {
1429
1430         # make sure priority for waiting or in-transit items is 0
1431         my $query = "
1432             UPDATE reserves
1433             SET    priority = 0
1434             WHERE reserve_id = ?
1435             AND found IN ('W', 'T')
1436         ";
1437         my $sth = $dbh->prepare($query);
1438         $sth->execute( $reserve_id );
1439     }
1440     my @priority;
1441
1442     # get whats left
1443     my $query = "
1444         SELECT reserve_id, borrowernumber, reservedate
1445         FROM   reserves
1446         WHERE  biblionumber   = ?
1447           AND  ((found <> 'W' AND found <> 'T') OR found IS NULL)
1448         ORDER BY priority ASC
1449     ";
1450     my $sth = $dbh->prepare($query);
1451     $sth->execute( $biblionumber );
1452     while ( my $line = $sth->fetchrow_hashref ) {
1453         push( @priority,     $line );
1454     }
1455
1456     # To find the matching index
1457     my $i;
1458     my $key = -1;    # to allow for 0 to be a valid result
1459     for ( $i = 0 ; $i < @priority ; $i++ ) {
1460         if ( $reserve_id == $priority[$i]->{'reserve_id'} ) {
1461             $key = $i;    # save the index
1462             last;
1463         }
1464     }
1465
1466     # if index exists in array then move it to new position
1467     if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1468         my $new_rank = $rank -
1469           1;    # $new_rank is what you want the new index to be in the array
1470         my $moving_item = splice( @priority, $key, 1 );
1471         splice( @priority, $new_rank, 0, $moving_item );
1472     }
1473
1474     # now fix the priority on those that are left....
1475     $query = "
1476         UPDATE reserves
1477         SET    priority = ?
1478         WHERE  reserve_id = ?
1479     ";
1480     $sth = $dbh->prepare($query);
1481     for ( my $j = 0 ; $j < @priority ; $j++ ) {
1482         $sth->execute(
1483             $j + 1,
1484             $priority[$j]->{'reserve_id'}
1485         );
1486     }
1487
1488     $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1489     $sth->execute();
1490
1491     unless ( $ignoreSetLowestRank ) {
1492       while ( my $res = $sth->fetchrow_hashref() ) {
1493         _FixPriority({
1494             reserve_id => $res->{'reserve_id'},
1495             rank => '999999',
1496             ignoreSetLowestRank => 1
1497         });
1498       }
1499     }
1500 }
1501
1502 =head2 _Findgroupreserve
1503
1504   @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead, $ignore_borrowers);
1505
1506 Looks for a holds-queue based item-specific match first, then for a holds-queue title-level match, returning the
1507 first match found.  If neither, then we look for non-holds-queue based holds.
1508 Lookahead is the number of days to look in advance.
1509
1510 C<&_Findgroupreserve> returns :
1511 C<@results> is an array of references-to-hash whose keys are mostly
1512 fields from the reserves table of the Koha database, plus
1513 C<biblioitemnumber>.
1514
1515 =cut
1516
1517 sub _Findgroupreserve {
1518     my ( $bibitem, $biblio, $itemnumber, $lookahead, $ignore_borrowers) = @_;
1519     my $dbh   = C4::Context->dbh;
1520
1521     # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1522     # check for exact targeted match
1523     my $item_level_target_query = qq{
1524         SELECT reserves.biblionumber        AS biblionumber,
1525                reserves.borrowernumber      AS borrowernumber,
1526                reserves.reservedate         AS reservedate,
1527                reserves.branchcode          AS branchcode,
1528                reserves.cancellationdate    AS cancellationdate,
1529                reserves.found               AS found,
1530                reserves.reservenotes        AS reservenotes,
1531                reserves.priority            AS priority,
1532                reserves.timestamp           AS timestamp,
1533                biblioitems.biblioitemnumber AS biblioitemnumber,
1534                reserves.itemnumber          AS itemnumber,
1535                reserves.reserve_id          AS reserve_id,
1536                reserves.itemtype            AS itemtype
1537         FROM reserves
1538         JOIN biblioitems USING (biblionumber)
1539         JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1540         WHERE found IS NULL
1541         AND priority > 0
1542         AND item_level_request = 1
1543         AND itemnumber = ?
1544         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1545         AND suspend = 0
1546         ORDER BY priority
1547     };
1548     my $sth = $dbh->prepare($item_level_target_query);
1549     $sth->execute($itemnumber, $lookahead||0);
1550     my @results;
1551     if ( my $data = $sth->fetchrow_hashref ) {
1552         push( @results, $data )
1553           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1554     }
1555     return @results if @results;
1556
1557     # check for title-level targeted match
1558     my $title_level_target_query = qq{
1559         SELECT reserves.biblionumber        AS biblionumber,
1560                reserves.borrowernumber      AS borrowernumber,
1561                reserves.reservedate         AS reservedate,
1562                reserves.branchcode          AS branchcode,
1563                reserves.cancellationdate    AS cancellationdate,
1564                reserves.found               AS found,
1565                reserves.reservenotes        AS reservenotes,
1566                reserves.priority            AS priority,
1567                reserves.timestamp           AS timestamp,
1568                biblioitems.biblioitemnumber AS biblioitemnumber,
1569                reserves.itemnumber          AS itemnumber,
1570                reserves.reserve_id          AS reserve_id,
1571                reserves.itemtype            AS itemtype
1572         FROM reserves
1573         JOIN biblioitems USING (biblionumber)
1574         JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1575         WHERE found IS NULL
1576         AND priority > 0
1577         AND item_level_request = 0
1578         AND hold_fill_targets.itemnumber = ?
1579         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1580         AND suspend = 0
1581         ORDER BY priority
1582     };
1583     $sth = $dbh->prepare($title_level_target_query);
1584     $sth->execute($itemnumber, $lookahead||0);
1585     @results = ();
1586     if ( my $data = $sth->fetchrow_hashref ) {
1587         push( @results, $data )
1588           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1589     }
1590     return @results if @results;
1591
1592     my $query = qq{
1593         SELECT reserves.biblionumber               AS biblionumber,
1594                reserves.borrowernumber             AS borrowernumber,
1595                reserves.reservedate                AS reservedate,
1596                reserves.waitingdate                AS waitingdate,
1597                reserves.branchcode                 AS branchcode,
1598                reserves.cancellationdate           AS cancellationdate,
1599                reserves.found                      AS found,
1600                reserves.reservenotes               AS reservenotes,
1601                reserves.priority                   AS priority,
1602                reserves.timestamp                  AS timestamp,
1603                reserves.itemnumber                 AS itemnumber,
1604                reserves.reserve_id                 AS reserve_id,
1605                reserves.itemtype                   AS itemtype
1606         FROM reserves
1607         WHERE reserves.biblionumber = ?
1608           AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1609           AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1610           AND suspend = 0
1611           ORDER BY priority
1612     };
1613     $sth = $dbh->prepare($query);
1614     $sth->execute( $biblio, $itemnumber, $lookahead||0);
1615     @results = ();
1616     while ( my $data = $sth->fetchrow_hashref ) {
1617         push( @results, $data )
1618           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1619     }
1620     return @results;
1621 }
1622
1623 =head2 _koha_notify_reserve
1624
1625   _koha_notify_reserve( $hold->reserve_id );
1626
1627 Sends a notification to the patron that their hold has been filled (through
1628 ModReserveAffect, _not_ ModReserveFill)
1629
1630 The letter code for this notice may be found using the following query:
1631
1632     select distinct letter_code
1633     from message_transports
1634     inner join message_attributes using (message_attribute_id)
1635     where message_name = 'Hold_Filled'
1636
1637 This will probably sipmly be 'HOLD', but because it is defined in the database,
1638 it is subject to addition or change.
1639
1640 The following tables are availalbe witin the notice:
1641
1642     branches
1643     borrowers
1644     biblio
1645     biblioitems
1646     reserves
1647     items
1648
1649 =cut
1650
1651 sub _koha_notify_reserve {
1652     my $reserve_id = shift;
1653     my $hold = Koha::Holds->find($reserve_id);
1654     my $borrowernumber = $hold->borrowernumber;
1655
1656     my $patron = Koha::Patrons->find( $borrowernumber );
1657
1658     # Try to get the borrower's email address
1659     my $to_address = $patron->notice_email_address;
1660
1661     my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
1662             borrowernumber => $borrowernumber,
1663             message_name => 'Hold_Filled'
1664     } );
1665
1666     my $library = Koha::Libraries->find( $hold->branchcode )->unblessed;
1667
1668     my $admin_email_address = $library->{branchemail} || C4::Context->preference('KohaAdminEmailAddress');
1669
1670     my %letter_params = (
1671         module => 'reserves',
1672         branchcode => $hold->branchcode,
1673         lang => $patron->lang,
1674         tables => {
1675             'branches'       => $library,
1676             'borrowers'      => $patron->unblessed,
1677             'biblio'         => $hold->biblionumber,
1678             'biblioitems'    => $hold->biblionumber,
1679             'reserves'       => $hold->unblessed,
1680             'items'          => $hold->itemnumber,
1681         },
1682     );
1683
1684     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.
1685     my $send_notification = sub {
1686         my ( $mtt, $letter_code ) = (@_);
1687         return unless defined $letter_code;
1688         $letter_params{letter_code} = $letter_code;
1689         $letter_params{message_transport_type} = $mtt;
1690         my $letter =  C4::Letters::GetPreparedLetter ( %letter_params );
1691         unless ($letter) {
1692             warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
1693             return;
1694         }
1695
1696         C4::Letters::EnqueueLetter( {
1697             letter => $letter,
1698             borrowernumber => $borrowernumber,
1699             from_address => $admin_email_address,
1700             message_transport_type => $mtt,
1701         } );
1702     };
1703
1704     while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
1705         next if (
1706                ( $mtt eq 'email' and not $to_address ) # No email address
1707             or ( $mtt eq 'sms'   and not $patron->smsalertnumber ) # No SMS number
1708             or ( $mtt eq 'phone' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl
1709         );
1710
1711         &$send_notification($mtt, $letter_code);
1712         $notification_sent++;
1713     }
1714     #Making sure that a print notification is sent if no other transport types can be utilized.
1715     if (! $notification_sent) {
1716         &$send_notification('print', 'HOLD');
1717     }
1718
1719 }
1720
1721 =head2 _ShiftPriorityByDateAndPriority
1722
1723   $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1724
1725 This increments the priority of all reserves after the one
1726 with either the lowest date after C<$reservedate>
1727 or the lowest priority after C<$priority>.
1728
1729 It effectively makes room for a new reserve to be inserted with a certain
1730 priority, which is returned.
1731
1732 This is most useful when the reservedate can be set by the user.  It allows
1733 the new reserve to be placed before other reserves that have a later
1734 reservedate.  Since priority also is set by the form in reserves/request.pl
1735 the sub accounts for that too.
1736
1737 =cut
1738
1739 sub _ShiftPriorityByDateAndPriority {
1740     my ( $biblio, $resdate, $new_priority ) = @_;
1741
1742     my $dbh = C4::Context->dbh;
1743     my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1744     my $sth = $dbh->prepare( $query );
1745     $sth->execute( $biblio, $resdate, $new_priority );
1746     my $min_priority = $sth->fetchrow;
1747     # if no such matches are found, $new_priority remains as original value
1748     $new_priority = $min_priority if ( $min_priority );
1749
1750     # Shift the priority up by one; works in conjunction with the next SQL statement
1751     $query = "UPDATE reserves
1752               SET priority = priority+1
1753               WHERE biblionumber = ?
1754               AND borrowernumber = ?
1755               AND reservedate = ?
1756               AND found IS NULL";
1757     my $sth_update = $dbh->prepare( $query );
1758
1759     # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1760     $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1761     $sth = $dbh->prepare( $query );
1762     $sth->execute( $new_priority, $biblio );
1763     while ( my $row = $sth->fetchrow_hashref ) {
1764         $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
1765     }
1766
1767     return $new_priority;  # so the caller knows what priority they wind up receiving
1768 }
1769
1770 =head2 MoveReserve
1771
1772   MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
1773
1774 Use when checking out an item to handle reserves
1775 If $cancelreserve boolean is set to true, it will remove existing reserve
1776
1777 =cut
1778
1779 sub MoveReserve {
1780     my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
1781
1782     my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
1783     my ( $restype, $res, $all_reserves ) = CheckReserves( $itemnumber, undef, $lookahead );
1784     return unless $res;
1785
1786     my $biblionumber     =  $res->{biblionumber};
1787
1788     if ($res->{borrowernumber} == $borrowernumber) {
1789         ModReserveFill($res);
1790     }
1791     else {
1792         # warn "Reserved";
1793         # The item is reserved by someone else.
1794         # Find this item in the reserves
1795
1796         my $borr_res;
1797         foreach (@$all_reserves) {
1798             $_->{'borrowernumber'} == $borrowernumber or next;
1799             $_->{'biblionumber'}   == $biblionumber   or next;
1800
1801             $borr_res = $_;
1802             last;
1803         }
1804
1805         if ( $borr_res ) {
1806             # The item is reserved by the current patron
1807             ModReserveFill($borr_res);
1808         }
1809
1810         if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
1811             RevertWaitingStatus({ itemnumber => $itemnumber });
1812         }
1813         elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
1814             my $hold = Koha::Holds->find( $res->{reserve_id} );
1815             $hold->cancel;
1816         }
1817     }
1818 }
1819
1820 =head2 MergeHolds
1821
1822   MergeHolds($dbh,$to_biblio, $from_biblio);
1823
1824 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
1825
1826 =cut
1827
1828 sub MergeHolds {
1829     my ( $dbh, $to_biblio, $from_biblio ) = @_;
1830     my $sth = $dbh->prepare(
1831         "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
1832     );
1833     $sth->execute($from_biblio);
1834     if ( my $data = $sth->fetchrow_hashref() ) {
1835
1836         # holds exist on old record, if not we don't need to do anything
1837         $sth = $dbh->prepare(
1838             "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
1839         $sth->execute( $to_biblio, $from_biblio );
1840
1841         # Reorder by date
1842         # don't reorder those already waiting
1843
1844         $sth = $dbh->prepare(
1845 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
1846         );
1847         my $upd_sth = $dbh->prepare(
1848 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
1849         AND reservedate = ? AND (itemnumber = ? or itemnumber is NULL) "
1850         );
1851         $sth->execute( $to_biblio, 'W', 'T' );
1852         my $priority = 1;
1853         while ( my $reserve = $sth->fetchrow_hashref() ) {
1854             $upd_sth->execute(
1855                 $priority,                    $to_biblio,
1856                 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
1857                 $reserve->{'itemnumber'}
1858             );
1859             $priority++;
1860         }
1861     }
1862 }
1863
1864 =head2 RevertWaitingStatus
1865
1866   RevertWaitingStatus({ itemnumber => $itemnumber });
1867
1868   Reverts a 'waiting' hold back to a regular hold with a priority of 1.
1869
1870   Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
1871           item level hold, even if it was only a bibliolevel hold to
1872           begin with. This is because we can no longer know if a hold
1873           was item-level or bib-level after a hold has been set to
1874           waiting status.
1875
1876 =cut
1877
1878 sub RevertWaitingStatus {
1879     my ( $params ) = @_;
1880     my $itemnumber = $params->{'itemnumber'};
1881
1882     return unless ( $itemnumber );
1883
1884     my $dbh = C4::Context->dbh;
1885
1886     ## Get the waiting reserve we want to revert
1887     my $query = "
1888         SELECT * FROM reserves
1889         WHERE itemnumber = ?
1890         AND found IS NOT NULL
1891     ";
1892     my $sth = $dbh->prepare( $query );
1893     $sth->execute( $itemnumber );
1894     my $reserve = $sth->fetchrow_hashref();
1895
1896     ## Increment the priority of all other non-waiting
1897     ## reserves for this bib record
1898     $query = "
1899         UPDATE reserves
1900         SET
1901           priority = priority + 1
1902         WHERE
1903           biblionumber =  ?
1904         AND
1905           priority > 0
1906     ";
1907     $sth = $dbh->prepare( $query );
1908     $sth->execute( $reserve->{'biblionumber'} );
1909
1910     ## Fix up the currently waiting reserve
1911     $query = "
1912     UPDATE reserves
1913     SET
1914       priority = 1,
1915       found = NULL,
1916       waitingdate = NULL
1917     WHERE
1918       reserve_id = ?
1919     ";
1920     $sth = $dbh->prepare( $query );
1921     $sth->execute( $reserve->{'reserve_id'} );
1922     _FixPriority( { biblionumber => $reserve->{biblionumber} } );
1923 }
1924
1925 =head2 ReserveSlip
1926
1927 ReserveSlip(
1928     {
1929         branchcode     => $branchcode,
1930         borrowernumber => $borrowernumber,
1931         biblionumber   => $biblionumber,
1932         [ itemnumber   => $itemnumber, ]
1933         [ barcode      => $barcode, ]
1934     }
1935   )
1936
1937 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
1938
1939 The letter code will be HOLD_SLIP, and the following tables are
1940 available within the slip:
1941
1942     reserves
1943     branches
1944     borrowers
1945     biblio
1946     biblioitems
1947     items
1948
1949 =cut
1950
1951 sub ReserveSlip {
1952     my ($args) = @_;
1953     my $branchcode     = $args->{branchcode};
1954     my $borrowernumber = $args->{borrowernumber};
1955     my $biblionumber   = $args->{biblionumber};
1956     my $itemnumber     = $args->{itemnumber};
1957     my $barcode        = $args->{barcode};
1958
1959
1960     my $patron = Koha::Patrons->find($borrowernumber);
1961
1962     my $hold;
1963     if ($itemnumber || $barcode ) {
1964         $itemnumber ||= Koha::Items->find( { barcode => $barcode } )->itemnumber;
1965
1966         $hold = Koha::Holds->search(
1967             {
1968                 biblionumber   => $biblionumber,
1969                 borrowernumber => $borrowernumber,
1970                 itemnumber     => $itemnumber
1971             }
1972         )->next;
1973     }
1974     else {
1975         $hold = Koha::Holds->search(
1976             {
1977                 biblionumber   => $biblionumber,
1978                 borrowernumber => $borrowernumber
1979             }
1980         )->next;
1981     }
1982
1983     return unless $hold;
1984     my $reserve = $hold->unblessed;
1985
1986     return  C4::Letters::GetPreparedLetter (
1987         module => 'circulation',
1988         letter_code => 'HOLD_SLIP',
1989         branchcode => $branchcode,
1990         lang => $patron->lang,
1991         tables => {
1992             'reserves'    => $reserve,
1993             'branches'    => $reserve->{branchcode},
1994             'borrowers'   => $reserve->{borrowernumber},
1995             'biblio'      => $reserve->{biblionumber},
1996             'biblioitems' => $reserve->{biblionumber},
1997             'items'       => $reserve->{itemnumber},
1998         },
1999     );
2000 }
2001
2002 =head2 GetReservesControlBranch
2003
2004   my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
2005
2006   Return the branchcode to be used to determine which reserves
2007   policy applies to a transaction.
2008
2009   C<$item> is a hashref for an item. Only 'homebranch' is used.
2010
2011   C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
2012
2013 =cut
2014
2015 sub GetReservesControlBranch {
2016     my ( $item, $borrower ) = @_;
2017
2018     my $reserves_control = C4::Context->preference('ReservesControlBranch');
2019
2020     my $branchcode =
2021         ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
2022       : ( $reserves_control eq 'PatronLibrary' )   ? $borrower->{'branchcode'}
2023       :                                              undef;
2024
2025     return $branchcode;
2026 }
2027
2028 =head2 CalculatePriority
2029
2030     my $p = CalculatePriority($biblionumber, $resdate);
2031
2032 Calculate priority for a new reserve on biblionumber, placing it at
2033 the end of the line of all holds whose start date falls before
2034 the current system time and that are neither on the hold shelf
2035 or in transit.
2036
2037 The reserve date parameter is optional; if it is supplied, the
2038 priority is based on the set of holds whose start date falls before
2039 the parameter value.
2040
2041 After calculation of this priority, it is recommended to call
2042 _ShiftPriorityByDateAndPriority. Note that this is currently done in
2043 AddReserves.
2044
2045 =cut
2046
2047 sub CalculatePriority {
2048     my ( $biblionumber, $resdate ) = @_;
2049
2050     my $sql = q{
2051         SELECT COUNT(*) FROM reserves
2052         WHERE biblionumber = ?
2053         AND   priority > 0
2054         AND   (found IS NULL OR found = '')
2055     };
2056     #skip found==W or found==T (waiting or transit holds)
2057     if( $resdate ) {
2058         $sql.= ' AND ( reservedate <= ? )';
2059     }
2060     else {
2061         $sql.= ' AND ( reservedate < NOW() )';
2062     }
2063     my $dbh = C4::Context->dbh();
2064     my @row = $dbh->selectrow_array(
2065         $sql,
2066         undef,
2067         $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2068     );
2069
2070     return @row ? $row[0]+1 : 1;
2071 }
2072
2073 =head2 IsItemOnHoldAndFound
2074
2075     my $bool = IsItemFoundHold( $itemnumber );
2076
2077     Returns true if the item is currently on hold
2078     and that hold has a non-null found status ( W, T, etc. )
2079
2080 =cut
2081
2082 sub IsItemOnHoldAndFound {
2083     my ($itemnumber) = @_;
2084
2085     my $rs = Koha::Database->new()->schema()->resultset('Reserve');
2086
2087     my $found = $rs->count(
2088         {
2089             itemnumber => $itemnumber,
2090             found      => { '!=' => undef }
2091         }
2092     );
2093
2094     return $found;
2095 }
2096
2097 =head2 GetMaxPatronHoldsForRecord
2098
2099 my $holds_per_record = ReservesControlBranch( $borrowernumber, $biblionumber );
2100
2101 For multiple holds on a given record for a given patron, the max
2102 number of record level holds that a patron can be placed is the highest
2103 value of the holds_per_record rule for each item if the record for that
2104 patron. This subroutine finds and returns the highest holds_per_record
2105 rule value for a given patron id and record id.
2106
2107 =cut
2108
2109 sub GetMaxPatronHoldsForRecord {
2110     my ( $borrowernumber, $biblionumber ) = @_;
2111
2112     my $patron = Koha::Patrons->find($borrowernumber);
2113     my @items = Koha::Items->search( { biblionumber => $biblionumber } );
2114
2115     my $controlbranch = C4::Context->preference('ReservesControlBranch');
2116
2117     my $categorycode = $patron->categorycode;
2118     my $branchcode;
2119     $branchcode = $patron->branchcode if ( $controlbranch eq "PatronLibrary" );
2120
2121     my $max = 0;
2122     foreach my $item (@items) {
2123         my $itemtype = $item->effective_itemtype();
2124
2125         $branchcode = $item->homebranch if ( $controlbranch eq "ItemHomeLibrary" );
2126
2127         my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2128         my $holds_per_record = $rule ? $rule->{holds_per_record} : 0;
2129         $max = $holds_per_record if $holds_per_record > $max;
2130     }
2131
2132     return $max;
2133 }
2134
2135 =head2 GetHoldRule
2136
2137 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2138
2139 Returns the matching hold related issuingrule fields for a given
2140 patron category, itemtype, and library.
2141
2142 =cut
2143
2144 sub GetHoldRule {
2145     my ( $categorycode, $itemtype, $branchcode ) = @_;
2146
2147     my $dbh = C4::Context->dbh;
2148
2149     my $sth = $dbh->prepare(
2150         q{
2151          SELECT categorycode, itemtype, branchcode, reservesallowed, holds_per_record, holds_per_day
2152            FROM issuingrules
2153           WHERE (categorycode in (?,'*') )
2154             AND (itemtype IN (?,'*'))
2155             AND (branchcode IN (?,'*'))
2156        ORDER BY categorycode DESC,
2157                 itemtype     DESC,
2158                 branchcode   DESC
2159         }
2160     );
2161
2162     $sth->execute( $categorycode, $itemtype, $branchcode );
2163
2164     return $sth->fetchrow_hashref();
2165 }
2166
2167 =head1 AUTHOR
2168
2169 Koha Development Team <http://koha-community.org/>
2170
2171 =cut
2172
2173 1;