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