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