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