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