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