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