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