Bug 25184: Items with a negative notforloan status should not be captured for holds
[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
178         if (
179             # If item is already checked out, it cannot be set waiting
180             !$item->onloan
181
182             # The item can't be waiting if it needs a transfer
183             && $item->holdingbranch eq $branch
184
185             # Similarly, if in transit it can't be waiting
186             && !$item->get_transfer
187
188             # If we can't hold damaged items, and it is damaged, it can't be waiting
189             && ( $item->damaged && C4::Context->preference('AllowHoldsOnDamagedItems') || !$item->damaged )
190
191             # Lastly, if this already has holds, we shouldn't make it waiting for the new hold
192             && !$item->current_holds->count )
193         {
194             $priority = 0;
195             $found = 'W';
196         }
197     }
198
199     if ( C4::Context->preference('AllowHoldDateInFuture') ) {
200
201         # Make room in reserves for this before those of a later reserve date
202         $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
203     }
204
205     my $waitingdate;
206
207     # If the reserv had the waiting status, we had the value of the resdate
208     if ( $found eq 'W' ) {
209         $waitingdate = $resdate;
210     }
211
212     # Don't add itemtype limit if specific item is selected
213     $itemtype = undef if $checkitem;
214
215     # updates take place here
216     my $hold = Koha::Hold->new(
217         {
218             borrowernumber => $borrowernumber,
219             biblionumber   => $biblionumber,
220             reservedate    => $resdate,
221             branchcode     => $branch,
222             priority       => $priority,
223             reservenotes   => $notes,
224             itemnumber     => $checkitem,
225             found          => $found,
226             waitingdate    => $waitingdate,
227             expirationdate => $expdate,
228             itemtype       => $itemtype,
229             item_level_hold => $checkitem ? 1 : 0,
230         }
231     )->store();
232     $hold->set_waiting() if $found eq 'W';
233
234     logaction( 'HOLDS', 'CREATE', $hold->id, Dumper($hold->unblessed) )
235         if C4::Context->preference('HoldsLog');
236
237     my $reserve_id = $hold->id();
238
239     # add a reserve fee if needed
240     if ( C4::Context->preference('HoldFeeMode') ne 'any_time_is_collected' ) {
241         my $reserve_fee = GetReserveFee( $borrowernumber, $biblionumber );
242         ChargeReserveFee( $borrowernumber, $reserve_fee, $title );
243     }
244
245     _FixPriority({ biblionumber => $biblionumber});
246
247     # Send e-mail to librarian if syspref is active
248     if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
249         my $patron = Koha::Patrons->find( $borrowernumber );
250         my $library = $patron->library;
251         if ( my $letter =  C4::Letters::GetPreparedLetter (
252             module => 'reserves',
253             letter_code => 'HOLDPLACED',
254             branchcode => $branch,
255             lang => $patron->lang,
256             tables => {
257                 'branches'    => $library->unblessed,
258                 'borrowers'   => $patron->unblessed,
259                 'biblio'      => $biblionumber,
260                 'biblioitems' => $biblionumber,
261                 'items'       => $checkitem,
262                 'reserves'    => $hold->unblessed,
263             },
264         ) ) {
265
266             my $admin_email_address = $library->branchemail || C4::Context->preference('KohaAdminEmailAddress');
267
268             C4::Letters::EnqueueLetter(
269                 {   letter                 => $letter,
270                     borrowernumber         => $borrowernumber,
271                     message_transport_type => 'email',
272                     from_address           => $admin_email_address,
273                     to_address           => $admin_email_address,
274                 }
275             );
276         }
277     }
278
279     return $reserve_id;
280 }
281
282 =head2 CanBookBeReserved
283
284   $canReserve = &CanBookBeReserved($borrowernumber, $biblionumber, $branchcode)
285   if ($canReserve eq 'OK') { #We can reserve this Item! }
286
287 See CanItemBeReserved() for possible return values.
288
289 =cut
290
291 sub CanBookBeReserved{
292     my ($borrowernumber, $biblionumber, $pickup_branchcode) = @_;
293
294     my @itemnumbers = Koha::Items->search({ biblionumber => $biblionumber})->get_column("itemnumber");
295     #get items linked via host records
296     my @hostitems = get_hostitemnumbers_of($biblionumber);
297     if (@hostitems){
298         push (@itemnumbers, @hostitems);
299     }
300
301     my $canReserve = { status => '' };
302     foreach my $itemnumber (@itemnumbers) {
303         $canReserve = CanItemBeReserved( $borrowernumber, $itemnumber, $pickup_branchcode );
304         return { status => 'OK' } if $canReserve->{status} eq 'OK';
305     }
306     return $canReserve;
307 }
308
309 =head2 CanItemBeReserved
310
311   $canReserve = &CanItemBeReserved($borrowernumber, $itemnumber, $branchcode)
312   if ($canReserve->{status} eq 'OK') { #We can reserve this Item! }
313
314 @RETURNS { status => OK },              if the Item can be reserved.
315          { status => ageRestricted },   if the Item is age restricted for this borrower.
316          { status => damaged },         if the Item is damaged.
317          { status => cannotReserveFromOtherBranches }, if syspref 'canreservefromotherbranches' is OK.
318          { status => tooManyReserves, limit => $limit }, if the borrower has exceeded their maximum reserve amount.
319          { status => notReservable },   if holds on this item are not allowed
320          { status => libraryNotFound },   if given branchcode is not an existing library
321          { status => libraryNotPickupLocation },   if given branchcode is not configured to be a pickup location
322          { status => cannotBeTransferred }, if branch transfer limit applies on given item and branchcode
323
324 =cut
325
326 sub CanItemBeReserved {
327     my ( $borrowernumber, $itemnumber, $pickup_branchcode ) = @_;
328
329     my $dbh = C4::Context->dbh;
330     my $ruleitemtype;    # itemtype of the matching issuing rule
331     my $allowedreserves  = 0; # Total number of holds allowed across all records
332     my $holds_per_record = 1; # Total number of holds allowed for this one given record
333     my $holds_per_day;        # Default to unlimited
334
335     # we retrieve borrowers and items informations #
336     # item->{itype} will come for biblioitems if necessery
337     my $item       = Koha::Items->find($itemnumber);
338     my $biblio     = $item->biblio;
339     my $patron = Koha::Patrons->find( $borrowernumber );
340     my $borrower = $patron->unblessed;
341
342     # If an item is damaged and we don't allow holds on damaged items, we can stop right here
343     return { status =>'damaged' }
344       if ( $item->damaged
345         && !C4::Context->preference('AllowHoldsOnDamagedItems') );
346
347     # Check for the age restriction
348     my ( $ageRestriction, $daysToAgeRestriction ) =
349       C4::Circulation::GetAgeRestriction( $biblio->biblioitem->agerestriction, $borrower );
350     return { status => 'ageRestricted' } if $daysToAgeRestriction && $daysToAgeRestriction > 0;
351
352     # Check that the patron doesn't have an item level hold on this item already
353     return { status =>'itemAlreadyOnHold' }
354       if Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->count();
355
356     my $controlbranch = C4::Context->preference('ReservesControlBranch');
357
358     my $querycount = q{
359         SELECT count(*) AS count
360           FROM reserves
361      LEFT JOIN items USING (itemnumber)
362      LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
363      LEFT JOIN borrowers USING (borrowernumber)
364          WHERE borrowernumber = ?
365     };
366
367     my $branchcode  = "";
368     my $branchfield = "reserves.branchcode";
369
370     if ( $controlbranch eq "ItemHomeLibrary" ) {
371         $branchfield = "items.homebranch";
372         $branchcode  = $item->homebranch;
373     }
374     elsif ( $controlbranch eq "PatronLibrary" ) {
375         $branchfield = "borrowers.branchcode";
376         $branchcode  = $borrower->{branchcode};
377     }
378
379     # we retrieve rights
380     if ( my $rights = GetHoldRule( $borrower->{'categorycode'}, $item->effective_itemtype, $branchcode ) ) {
381         $ruleitemtype     = $rights->{itemtype};
382         $allowedreserves  = $rights->{reservesallowed};
383         $holds_per_record = $rights->{holds_per_record};
384         $holds_per_day    = $rights->{holds_per_day};
385     }
386     else {
387         $ruleitemtype = '*';
388     }
389
390     my $holds = Koha::Holds->search(
391         {
392             borrowernumber => $borrowernumber,
393             biblionumber   => $item->biblionumber,
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 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 => 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     logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->unblessed) )
1018         if C4::Context->preference('HoldsLog');
1019
1020     # FIXME Must call Koha::Hold->cancel ? => No, should call ->filled and add the correct log
1021     Koha::Old::Hold->new( $hold->unblessed() )->store();
1022
1023     $hold->delete();
1024
1025     if ( C4::Context->preference('HoldFeeMode') eq 'any_time_is_collected' ) {
1026         my $reserve_fee = GetReserveFee( $hold->borrowernumber, $hold->biblionumber );
1027         ChargeReserveFee( $hold->borrowernumber, $reserve_fee, $hold->biblio->title );
1028     }
1029
1030     # now fix the priority on the others (if the priority wasn't
1031     # already sorted!)....
1032     unless ( $priority == 0 ) {
1033         _FixPriority( { reserve_id => $reserve_id, biblionumber => $hold->biblionumber } );
1034     }
1035 }
1036
1037 =head2 ModReserveStatus
1038
1039   &ModReserveStatus($itemnumber, $newstatus);
1040
1041 Update the reserve status for the active (priority=0) reserve.
1042
1043 $itemnumber is the itemnumber the reserve is on
1044
1045 $newstatus is the new status.
1046
1047 =cut
1048
1049 sub ModReserveStatus {
1050
1051     #first : check if we have a reservation for this item .
1052     my ($itemnumber, $newstatus) = @_;
1053     my $dbh = C4::Context->dbh;
1054
1055     my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1056     my $sth_set = $dbh->prepare($query);
1057     $sth_set->execute( $newstatus, $itemnumber );
1058
1059     my $item = Koha::Items->find($itemnumber);
1060     if ( ( $item->location eq 'CART' && $item->permanent_location ne 'CART'  ) && $newstatus ) {
1061       CartToShelf( $itemnumber );
1062     }
1063 }
1064
1065 =head2 ModReserveAffect
1066
1067   &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend,$reserve_id);
1068
1069 This function affect an item and a status for a given reserve, either fetched directly
1070 by record_id, or by borrowernumber and itemnumber or biblionumber. If only biblionumber
1071 is given, only first reserve returned is affected, which is ok for anything but
1072 multi-item holds.
1073
1074 if $transferToDo is not set, then the status is set to "Waiting" as well.
1075 otherwise, a transfer is on the way, and the end of the transfer will
1076 take care of the waiting status
1077
1078 =cut
1079
1080 sub ModReserveAffect {
1081     my ( $itemnumber, $borrowernumber, $transferToDo, $reserve_id ) = @_;
1082     my $dbh = C4::Context->dbh;
1083
1084     # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1085     # attached to $itemnumber
1086     my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1087     $sth->execute($itemnumber);
1088     my ($biblionumber) = $sth->fetchrow;
1089
1090     # get request - need to find out if item is already
1091     # waiting in order to not send duplicate hold filled notifications
1092
1093     my $hold;
1094     # Find hold by id if we have it
1095     $hold = Koha::Holds->find( $reserve_id ) if $reserve_id;
1096     # Find item level hold for this item if there is one
1097     $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->next();
1098     # Find record level hold if there is no item level hold
1099     $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->next();
1100
1101     return unless $hold;
1102
1103     my $already_on_shelf = $hold->found && $hold->found eq 'W';
1104
1105     $hold->itemnumber($itemnumber);
1106     $hold->set_waiting($transferToDo);
1107
1108     if( !$transferToDo ){
1109         _koha_notify_reserve( $hold->reserve_id ) unless $already_on_shelf;
1110         my $transfers = Koha::Item::Transfers->search({
1111             itemnumber => $itemnumber,
1112             datearrived => undef
1113         });
1114         while( my $transfer = $transfers->next ){
1115             $transfer->datearrived( dt_from_string() )->store;
1116         };
1117     }
1118
1119
1120     _FixPriority( { biblionumber => $biblionumber } );
1121     my $item = Koha::Items->find($itemnumber);
1122     if ( ( $item->location eq 'CART' && $item->permanent_location ne 'CART'  ) ) {
1123       CartToShelf( $itemnumber );
1124     }
1125
1126     logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->unblessed) )
1127         if C4::Context->preference('HoldsLog');
1128
1129     return;
1130 }
1131
1132 =head2 ModReserveCancelAll
1133
1134   ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1135
1136 function to cancel reserv,check other reserves, and transfer document if it's necessary
1137
1138 =cut
1139
1140 sub ModReserveCancelAll {
1141     my $messages;
1142     my $nextreservinfo;
1143     my ( $itemnumber, $borrowernumber ) = @_;
1144
1145     #step 1 : cancel the reservation
1146     my $holds = Koha::Holds->search({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1147     return unless $holds->count;
1148     $holds->next->cancel;
1149
1150     #step 2 launch the subroutine of the others reserves
1151     ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1152
1153     return ( $messages, $nextreservinfo );
1154 }
1155
1156 =head2 ModReserveMinusPriority
1157
1158   &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1159
1160 Reduce the values of queued list
1161
1162 =cut
1163
1164 sub ModReserveMinusPriority {
1165     my ( $itemnumber, $reserve_id ) = @_;
1166
1167     #first step update the value of the first person on reserv
1168     my $dbh   = C4::Context->dbh;
1169     my $query = "
1170         UPDATE reserves
1171         SET    priority = 0 , itemnumber = ?
1172         WHERE  reserve_id = ?
1173     ";
1174     my $sth_upd = $dbh->prepare($query);
1175     $sth_upd->execute( $itemnumber, $reserve_id );
1176     # second step update all others reserves
1177     _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1178 }
1179
1180 =head2 IsAvailableForItemLevelRequest
1181
1182   my $is_available = IsAvailableForItemLevelRequest( $item_record, $borrower_record, $pickup_branchcode );
1183
1184 Checks whether a given item record is available for an
1185 item-level hold request.  An item is available if
1186
1187 * it is not lost AND
1188 * it is not damaged AND
1189 * it is not withdrawn AND
1190 * a waiting or in transit reserve is placed on
1191 * does not have a not for loan value > 0
1192
1193 Need to check the issuingrules onshelfholds column,
1194 if this is set items on the shelf can be placed on hold
1195
1196 Note that IsAvailableForItemLevelRequest() does not
1197 check if the staff operator is authorized to place
1198 a request on the item - in particular,
1199 this routine does not check IndependentBranches
1200 and canreservefromotherbranches.
1201
1202 =cut
1203
1204 sub IsAvailableForItemLevelRequest {
1205     my ( $item, $patron, $pickup_branchcode ) = @_;
1206
1207     my $dbh = C4::Context->dbh;
1208     # must check the notforloan setting of the itemtype
1209     # FIXME - a lot of places in the code do this
1210     #         or something similar - need to be
1211     #         consolidated
1212     my $itemtype = $item->effective_itemtype;
1213     my $notforloan_per_itemtype = Koha::ItemTypes->find($itemtype)->notforloan;
1214
1215     return 0 if
1216         $notforloan_per_itemtype ||
1217         $item->itemlost        ||
1218         $item->notforloan > 0  ||
1219         $item->withdrawn        ||
1220         ($item->damaged && !C4::Context->preference('AllowHoldsOnDamagedItems'));
1221
1222     my $on_shelf_holds = Koha::IssuingRules->get_onshelfholds_policy( { item => $item, patron => $patron } );
1223
1224     if ($pickup_branchcode) {
1225         my $destination = Koha::Libraries->find($pickup_branchcode);
1226         return 0 unless $destination;
1227         return 0 unless $destination->pickup_location;
1228         return 0 unless $item->can_be_transferred( { to => $destination } );
1229     }
1230
1231     if ( $on_shelf_holds == 1 ) {
1232         return 1;
1233     } elsif ( $on_shelf_holds == 2 ) {
1234         my @items =
1235           Koha::Items->search( { biblionumber => $item->biblionumber } );
1236
1237         my $any_available = 0;
1238
1239         foreach my $i (@items) {
1240             my $reserves_control_branch = GetReservesControlBranch( $i->unblessed(), $patron->unblessed );
1241             my $branchitemrule = C4::Circulation::GetBranchItemRule( $reserves_control_branch, $i->itype );
1242
1243             $any_available = 1
1244               unless $i->itemlost
1245               || $i->notforloan > 0
1246               || $i->withdrawn
1247               || $i->onloan
1248               || IsItemOnHoldAndFound( $i->id )
1249               || ( $i->damaged
1250                 && !C4::Context->preference('AllowHoldsOnDamagedItems') )
1251               || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan
1252               || $branchitemrule->{holdallowed} == 1 && $patron->branchcode ne $i->homebranch;
1253         }
1254
1255         return $any_available ? 0 : 1;
1256     } else { # on_shelf_holds == 0 "If any unavailable" (the description is rather cryptic and could still be improved)
1257         return $item->onloan || IsItemOnHoldAndFound( $item->itemnumber );
1258     }
1259 }
1260
1261 sub _get_itype {
1262     my $item = shift;
1263
1264     my $itype;
1265     if (C4::Context->preference('item-level_itypes')) {
1266         # We can't trust GetItem to honour the syspref, so safest to do it ourselves
1267         # When GetItem is fixed, we can remove this
1268         $itype = $item->{itype};
1269     }
1270     else {
1271         # XXX This is a bit dodgy. It relies on biblio itemtype column having different name.
1272         # So if we already have a biblioitems join when calling this function,
1273         # we don't need to access the database again
1274         $itype = $item->{itemtype};
1275     }
1276     unless ($itype) {
1277         my $dbh = C4::Context->dbh;
1278         my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? ";
1279         my $sth = $dbh->prepare($query);
1280         $sth->execute($item->{biblioitemnumber});
1281         if (my $data = $sth->fetchrow_hashref()){
1282             $itype = $data->{itemtype};
1283         }
1284     }
1285     return $itype;
1286 }
1287
1288 =head2 AlterPriority
1289
1290   AlterPriority( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority );
1291
1292 This function changes a reserve's priority up, down, to the top, or to the bottom.
1293 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1294
1295 =cut
1296
1297 sub AlterPriority {
1298     my ( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority ) = @_;
1299
1300     my $hold = Koha::Holds->find( $reserve_id );
1301     return unless $hold;
1302
1303     if ( $hold->cancellationdate ) {
1304         warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (" . $hold->cancellationdate . ')';
1305         return;
1306     }
1307
1308     if ( $where eq 'up' ) {
1309       return unless $prev_priority;
1310       _FixPriority({ reserve_id => $reserve_id, rank => $prev_priority })
1311     } elsif ( $where eq 'down' ) {
1312       return unless $next_priority;
1313       _FixPriority({ reserve_id => $reserve_id, rank => $next_priority })
1314     } elsif ( $where eq 'top' ) {
1315       _FixPriority({ reserve_id => $reserve_id, rank => $first_priority })
1316     } elsif ( $where eq 'bottom' ) {
1317       _FixPriority({ reserve_id => $reserve_id, rank => $last_priority });
1318     }
1319
1320     # FIXME Should return the new priority
1321 }
1322
1323 =head2 ToggleLowestPriority
1324
1325   ToggleLowestPriority( $borrowernumber, $biblionumber );
1326
1327 This function sets the lowestPriority field to true if is false, and false if it is true.
1328
1329 =cut
1330
1331 sub ToggleLowestPriority {
1332     my ( $reserve_id ) = @_;
1333
1334     my $dbh = C4::Context->dbh;
1335
1336     my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?");
1337     $sth->execute( $reserve_id );
1338
1339     _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1340 }
1341
1342 =head2 ToggleSuspend
1343
1344   ToggleSuspend( $reserve_id );
1345
1346 This function sets the suspend field to true if is false, and false if it is true.
1347 If the reserve is currently suspended with a suspend_until date, that date will
1348 be cleared when it is unsuspended.
1349
1350 =cut
1351
1352 sub ToggleSuspend {
1353     my ( $reserve_id, $suspend_until ) = @_;
1354
1355     $suspend_until = dt_from_string($suspend_until) if ($suspend_until);
1356
1357     my $hold = Koha::Holds->find( $reserve_id );
1358
1359     if ( $hold->is_suspended ) {
1360         $hold->resume()
1361     } else {
1362         $hold->suspend_hold( $suspend_until );
1363     }
1364 }
1365
1366 =head2 SuspendAll
1367
1368   SuspendAll(
1369       borrowernumber   => $borrowernumber,
1370       [ biblionumber   => $biblionumber, ]
1371       [ suspend_until  => $suspend_until, ]
1372       [ suspend        => $suspend ]
1373   );
1374
1375   This function accepts a set of hash keys as its parameters.
1376   It requires either borrowernumber or biblionumber, or both.
1377
1378   suspend_until is wholly optional.
1379
1380 =cut
1381
1382 sub SuspendAll {
1383     my %params = @_;
1384
1385     my $borrowernumber = $params{'borrowernumber'} || undef;
1386     my $biblionumber   = $params{'biblionumber'}   || undef;
1387     my $suspend_until  = $params{'suspend_until'}  || undef;
1388     my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1;
1389
1390     $suspend_until = eval { dt_from_string($suspend_until) }
1391       if ( defined($suspend_until) );
1392
1393     return unless ( $borrowernumber || $biblionumber );
1394
1395     my $params;
1396     $params->{found}          = undef;
1397     $params->{borrowernumber} = $borrowernumber if $borrowernumber;
1398     $params->{biblionumber}   = $biblionumber if $biblionumber;
1399
1400     my @holds = Koha::Holds->search($params);
1401
1402     if ($suspend) {
1403         map { $_->suspend_hold($suspend_until) } @holds;
1404     }
1405     else {
1406         map { $_->resume() } @holds;
1407     }
1408 }
1409
1410
1411 =head2 _FixPriority
1412
1413   _FixPriority({
1414     reserve_id => $reserve_id,
1415     [rank => $rank,]
1416     [ignoreSetLowestRank => $ignoreSetLowestRank]
1417   });
1418
1419   or
1420
1421   _FixPriority({ biblionumber => $biblionumber});
1422
1423 This routine adjusts the priority of a hold request and holds
1424 on the same bib.
1425
1426 In the first form, where a reserve_id is passed, the priority of the
1427 hold is set to supplied rank, and other holds for that bib are adjusted
1428 accordingly.  If the rank is "del", the hold is cancelled.  If no rank
1429 is supplied, all of the holds on that bib have their priority adjusted
1430 as if the second form had been used.
1431
1432 In the second form, where a biblionumber is passed, the holds on that
1433 bib (that are not captured) are sorted in order of increasing priority,
1434 then have reserves.priority set so that the first non-captured hold
1435 has its priority set to 1, the second non-captured hold has its priority
1436 set to 2, and so forth.
1437
1438 In both cases, holds that have the lowestPriority flag on are have their
1439 priority adjusted to ensure that they remain at the end of the line.
1440
1441 Note that the ignoreSetLowestRank parameter is meant to be used only
1442 when _FixPriority calls itself.
1443
1444 =cut
1445
1446 sub _FixPriority {
1447     my ( $params ) = @_;
1448     my $reserve_id = $params->{reserve_id};
1449     my $rank = $params->{rank} // '';
1450     my $ignoreSetLowestRank = $params->{ignoreSetLowestRank};
1451     my $biblionumber = $params->{biblionumber};
1452
1453     my $dbh = C4::Context->dbh;
1454
1455     my $hold;
1456     if ( $reserve_id ) {
1457         $hold = Koha::Holds->find( $reserve_id );
1458         if (!defined $hold){
1459             # may have already been checked out and hold fulfilled
1460             $hold = Koha::Old::Holds->find( $reserve_id );
1461         }
1462         return unless $hold;
1463     }
1464
1465     unless ( $biblionumber ) { # FIXME This is a very weird API
1466         $biblionumber = $hold->biblionumber;
1467     }
1468
1469     if ( $rank eq "del" ) { # FIXME will crash if called without $hold
1470         $hold->cancel;
1471     }
1472     elsif ( $rank eq "W" || $rank eq "0" ) {
1473
1474         # make sure priority for waiting or in-transit items is 0
1475         my $query = "
1476             UPDATE reserves
1477             SET    priority = 0
1478             WHERE reserve_id = ?
1479             AND found IN ('W', 'T')
1480         ";
1481         my $sth = $dbh->prepare($query);
1482         $sth->execute( $reserve_id );
1483     }
1484     my @priority;
1485
1486     # get whats left
1487     my $query = "
1488         SELECT reserve_id, borrowernumber, reservedate
1489         FROM   reserves
1490         WHERE  biblionumber   = ?
1491           AND  ((found <> 'W' AND found <> 'T') OR found IS NULL)
1492         ORDER BY priority ASC
1493     ";
1494     my $sth = $dbh->prepare($query);
1495     $sth->execute( $biblionumber );
1496     while ( my $line = $sth->fetchrow_hashref ) {
1497         push( @priority,     $line );
1498     }
1499
1500     # To find the matching index
1501     my $i;
1502     my $key = -1;    # to allow for 0 to be a valid result
1503     for ( $i = 0 ; $i < @priority ; $i++ ) {
1504         if ( $reserve_id == $priority[$i]->{'reserve_id'} ) {
1505             $key = $i;    # save the index
1506             last;
1507         }
1508     }
1509
1510     # if index exists in array then move it to new position
1511     if ( $key > -1 && $rank ne 'del' && $rank > 0 ) {
1512         my $new_rank = $rank -
1513           1;    # $new_rank is what you want the new index to be in the array
1514         my $moving_item = splice( @priority, $key, 1 );
1515         splice( @priority, $new_rank, 0, $moving_item );
1516     }
1517
1518     # now fix the priority on those that are left....
1519     $query = "
1520         UPDATE reserves
1521         SET    priority = ?
1522         WHERE  reserve_id = ?
1523     ";
1524     $sth = $dbh->prepare($query);
1525     for ( my $j = 0 ; $j < @priority ; $j++ ) {
1526         $sth->execute(
1527             $j + 1,
1528             $priority[$j]->{'reserve_id'}
1529         );
1530     }
1531
1532     $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" );
1533     $sth->execute();
1534
1535     unless ( $ignoreSetLowestRank ) {
1536       while ( my $res = $sth->fetchrow_hashref() ) {
1537         _FixPriority({
1538             reserve_id => $res->{'reserve_id'},
1539             rank => '999999',
1540             ignoreSetLowestRank => 1
1541         });
1542       }
1543     }
1544 }
1545
1546 =head2 _Findgroupreserve
1547
1548   @results = &_Findgroupreserve($biblioitemnumber, $biblionumber, $itemnumber, $lookahead, $ignore_borrowers);
1549
1550 Looks for a holds-queue based item-specific match first, then for a holds-queue title-level match, returning the
1551 first match found.  If neither, then we look for non-holds-queue based holds.
1552 Lookahead is the number of days to look in advance.
1553
1554 C<&_Findgroupreserve> returns :
1555 C<@results> is an array of references-to-hash whose keys are mostly
1556 fields from the reserves table of the Koha database, plus
1557 C<biblioitemnumber>.
1558
1559 This routine with either return:
1560 1 - Item specific holds from the holds queue
1561 2 - Title level holds from the holds queue
1562 3 - All holds for this biblionumber
1563
1564 All return values will respect any borrowernumbers passed as arrayref in $ignore_borrowers
1565
1566 =cut
1567
1568 sub _Findgroupreserve {
1569     my ( $bibitem, $biblio, $itemnumber, $lookahead, $ignore_borrowers) = @_;
1570     my $dbh   = C4::Context->dbh;
1571
1572     # TODO: consolidate at least the SELECT portion of the first 2 queries to a common $select var.
1573     # check for exact targeted match
1574     my $item_level_target_query = qq{
1575         SELECT reserves.biblionumber        AS biblionumber,
1576                reserves.borrowernumber      AS borrowernumber,
1577                reserves.reservedate         AS reservedate,
1578                reserves.branchcode          AS branchcode,
1579                reserves.cancellationdate    AS cancellationdate,
1580                reserves.found               AS found,
1581                reserves.reservenotes        AS reservenotes,
1582                reserves.priority            AS priority,
1583                reserves.timestamp           AS timestamp,
1584                biblioitems.biblioitemnumber AS biblioitemnumber,
1585                reserves.itemnumber          AS itemnumber,
1586                reserves.reserve_id          AS reserve_id,
1587                reserves.itemtype            AS itemtype
1588         FROM reserves
1589         JOIN biblioitems USING (biblionumber)
1590         JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber)
1591         WHERE found IS NULL
1592         AND priority > 0
1593         AND item_level_request = 1
1594         AND itemnumber = ?
1595         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1596         AND suspend = 0
1597         ORDER BY priority
1598     };
1599     my $sth = $dbh->prepare($item_level_target_query);
1600     $sth->execute($itemnumber, $lookahead||0);
1601     my @results;
1602     if ( my $data = $sth->fetchrow_hashref ) {
1603         push( @results, $data )
1604           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1605     }
1606     return @results if @results;
1607
1608     # check for title-level targeted match
1609     my $title_level_target_query = qq{
1610         SELECT reserves.biblionumber        AS biblionumber,
1611                reserves.borrowernumber      AS borrowernumber,
1612                reserves.reservedate         AS reservedate,
1613                reserves.branchcode          AS branchcode,
1614                reserves.cancellationdate    AS cancellationdate,
1615                reserves.found               AS found,
1616                reserves.reservenotes        AS reservenotes,
1617                reserves.priority            AS priority,
1618                reserves.timestamp           AS timestamp,
1619                biblioitems.biblioitemnumber AS biblioitemnumber,
1620                reserves.itemnumber          AS itemnumber,
1621                reserves.reserve_id          AS reserve_id,
1622                reserves.itemtype            AS itemtype
1623         FROM reserves
1624         JOIN biblioitems USING (biblionumber)
1625         JOIN hold_fill_targets USING (biblionumber, borrowernumber)
1626         WHERE found IS NULL
1627         AND priority > 0
1628         AND item_level_request = 0
1629         AND hold_fill_targets.itemnumber = ?
1630         AND reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1631         AND suspend = 0
1632         ORDER BY priority
1633     };
1634     $sth = $dbh->prepare($title_level_target_query);
1635     $sth->execute($itemnumber, $lookahead||0);
1636     @results = ();
1637     if ( my $data = $sth->fetchrow_hashref ) {
1638         push( @results, $data )
1639           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1640     }
1641     return @results if @results;
1642
1643     my $query = qq{
1644         SELECT reserves.biblionumber               AS biblionumber,
1645                reserves.borrowernumber             AS borrowernumber,
1646                reserves.reservedate                AS reservedate,
1647                reserves.waitingdate                AS waitingdate,
1648                reserves.branchcode                 AS branchcode,
1649                reserves.cancellationdate           AS cancellationdate,
1650                reserves.found                      AS found,
1651                reserves.reservenotes               AS reservenotes,
1652                reserves.priority                   AS priority,
1653                reserves.timestamp                  AS timestamp,
1654                reserves.itemnumber                 AS itemnumber,
1655                reserves.reserve_id                 AS reserve_id,
1656                reserves.itemtype                   AS itemtype
1657         FROM reserves
1658         WHERE reserves.biblionumber = ?
1659           AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?)
1660           AND reserves.reservedate <= DATE_ADD(NOW(),INTERVAL ? DAY)
1661           AND suspend = 0
1662           ORDER BY priority
1663     };
1664     $sth = $dbh->prepare($query);
1665     $sth->execute( $biblio, $itemnumber, $lookahead||0);
1666     @results = ();
1667     while ( my $data = $sth->fetchrow_hashref ) {
1668         push( @results, $data )
1669           unless any{ $data->{borrowernumber} eq $_ } @$ignore_borrowers ;
1670     }
1671     return @results;
1672 }
1673
1674 =head2 _koha_notify_reserve
1675
1676   _koha_notify_reserve( $hold->reserve_id );
1677
1678 Sends a notification to the patron that their hold has been filled (through
1679 ModReserveAffect, _not_ ModReserveFill)
1680
1681 The letter code for this notice may be found using the following query:
1682
1683     select distinct letter_code
1684     from message_transports
1685     inner join message_attributes using (message_attribute_id)
1686     where message_name = 'Hold_Filled'
1687
1688 This will probably sipmly be 'HOLD', but because it is defined in the database,
1689 it is subject to addition or change.
1690
1691 The following tables are availalbe witin the notice:
1692
1693     branches
1694     borrowers
1695     biblio
1696     biblioitems
1697     reserves
1698     items
1699
1700 =cut
1701
1702 sub _koha_notify_reserve {
1703     my $reserve_id = shift;
1704     my $hold = Koha::Holds->find($reserve_id);
1705     my $borrowernumber = $hold->borrowernumber;
1706
1707     my $patron = Koha::Patrons->find( $borrowernumber );
1708
1709     # Try to get the borrower's email address
1710     my $to_address = $patron->notice_email_address;
1711
1712     my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( {
1713             borrowernumber => $borrowernumber,
1714             message_name => 'Hold_Filled'
1715     } );
1716
1717     my $library = Koha::Libraries->find( $hold->branchcode )->unblessed;
1718
1719     my $admin_email_address = $library->{branchemail} || C4::Context->preference('KohaAdminEmailAddress');
1720
1721     my %letter_params = (
1722         module => 'reserves',
1723         branchcode => $hold->branchcode,
1724         lang => $patron->lang,
1725         tables => {
1726             'branches'       => $library,
1727             'borrowers'      => $patron->unblessed,
1728             'biblio'         => $hold->biblionumber,
1729             'biblioitems'    => $hold->biblionumber,
1730             'reserves'       => $hold->unblessed,
1731             'items'          => $hold->itemnumber,
1732         },
1733     );
1734
1735     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.
1736     my $send_notification = sub {
1737         my ( $mtt, $letter_code ) = (@_);
1738         return unless defined $letter_code;
1739         $letter_params{letter_code} = $letter_code;
1740         $letter_params{message_transport_type} = $mtt;
1741         my $letter =  C4::Letters::GetPreparedLetter ( %letter_params );
1742         unless ($letter) {
1743             warn "Could not find a letter called '$letter_params{'letter_code'}' for $mtt in the 'reserves' module";
1744             return;
1745         }
1746
1747         C4::Letters::EnqueueLetter( {
1748             letter => $letter,
1749             borrowernumber => $borrowernumber,
1750             from_address => $admin_email_address,
1751             message_transport_type => $mtt,
1752         } );
1753     };
1754
1755     while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) {
1756         next if (
1757                ( $mtt eq 'email' and not $to_address ) # No email address
1758             or ( $mtt eq 'sms'   and not $patron->smsalertnumber ) # No SMS number
1759             or ( $mtt eq 'phone' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl
1760         );
1761
1762         &$send_notification($mtt, $letter_code);
1763         $notification_sent++;
1764     }
1765     #Making sure that a print notification is sent if no other transport types can be utilized.
1766     if (! $notification_sent) {
1767         &$send_notification('print', 'HOLD');
1768     }
1769
1770 }
1771
1772 =head2 _ShiftPriorityByDateAndPriority
1773
1774   $new_priority = _ShiftPriorityByDateAndPriority( $biblionumber, $reservedate, $priority );
1775
1776 This increments the priority of all reserves after the one
1777 with either the lowest date after C<$reservedate>
1778 or the lowest priority after C<$priority>.
1779
1780 It effectively makes room for a new reserve to be inserted with a certain
1781 priority, which is returned.
1782
1783 This is most useful when the reservedate can be set by the user.  It allows
1784 the new reserve to be placed before other reserves that have a later
1785 reservedate.  Since priority also is set by the form in reserves/request.pl
1786 the sub accounts for that too.
1787
1788 =cut
1789
1790 sub _ShiftPriorityByDateAndPriority {
1791     my ( $biblio, $resdate, $new_priority ) = @_;
1792
1793     my $dbh = C4::Context->dbh;
1794     my $query = "SELECT priority FROM reserves WHERE biblionumber = ? AND ( reservedate > ? OR priority > ? ) ORDER BY priority ASC LIMIT 1";
1795     my $sth = $dbh->prepare( $query );
1796     $sth->execute( $biblio, $resdate, $new_priority );
1797     my $min_priority = $sth->fetchrow;
1798     # if no such matches are found, $new_priority remains as original value
1799     $new_priority = $min_priority if ( $min_priority );
1800
1801     # Shift the priority up by one; works in conjunction with the next SQL statement
1802     $query = "UPDATE reserves
1803               SET priority = priority+1
1804               WHERE biblionumber = ?
1805               AND borrowernumber = ?
1806               AND reservedate = ?
1807               AND found IS NULL";
1808     my $sth_update = $dbh->prepare( $query );
1809
1810     # Select all reserves for the biblio with priority greater than $new_priority, and order greatest to least
1811     $query = "SELECT borrowernumber, reservedate FROM reserves WHERE priority >= ? AND biblionumber = ? ORDER BY priority DESC";
1812     $sth = $dbh->prepare( $query );
1813     $sth->execute( $new_priority, $biblio );
1814     while ( my $row = $sth->fetchrow_hashref ) {
1815         $sth_update->execute( $biblio, $row->{borrowernumber}, $row->{reservedate} );
1816     }
1817
1818     return $new_priority;  # so the caller knows what priority they wind up receiving
1819 }
1820
1821 =head2 MoveReserve
1822
1823   MoveReserve( $itemnumber, $borrowernumber, $cancelreserve )
1824
1825 Use when checking out an item to handle reserves
1826 If $cancelreserve boolean is set to true, it will remove existing reserve
1827
1828 =cut
1829
1830 sub MoveReserve {
1831     my ( $itemnumber, $borrowernumber, $cancelreserve ) = @_;
1832
1833     my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
1834     my ( $restype, $res, undef ) = CheckReserves( $itemnumber, undef, $lookahead );
1835     return unless $res;
1836
1837     my $biblionumber     =  $res->{biblionumber};
1838
1839     if ($res->{borrowernumber} == $borrowernumber) {
1840         ModReserveFill($res);
1841     }
1842     else {
1843         # warn "Reserved";
1844         # The item is reserved by someone else.
1845         # Find this item in the reserves
1846
1847         my $borr_res  = Koha::Holds->search({
1848             borrowernumber => $borrowernumber,
1849             biblionumber   => $biblionumber,
1850         },{
1851             order_by       => 'priority'
1852         })->next();
1853
1854         if ( $borr_res ) {
1855             # The item is reserved by the current patron
1856             ModReserveFill($borr_res->unblessed);
1857         }
1858
1859         if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
1860             RevertWaitingStatus({ itemnumber => $itemnumber });
1861         }
1862         elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
1863             my $hold = Koha::Holds->find( $res->{reserve_id} );
1864             $hold->cancel;
1865         }
1866     }
1867 }
1868
1869 =head2 MergeHolds
1870
1871   MergeHolds($dbh,$to_biblio, $from_biblio);
1872
1873 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
1874
1875 =cut
1876
1877 sub MergeHolds {
1878     my ( $dbh, $to_biblio, $from_biblio ) = @_;
1879     my $sth = $dbh->prepare(
1880         "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
1881     );
1882     $sth->execute($from_biblio);
1883     if ( my $data = $sth->fetchrow_hashref() ) {
1884
1885         # holds exist on old record, if not we don't need to do anything
1886         $sth = $dbh->prepare(
1887             "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
1888         $sth->execute( $to_biblio, $from_biblio );
1889
1890         # Reorder by date
1891         # don't reorder those already waiting
1892
1893         $sth = $dbh->prepare(
1894 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
1895         );
1896         my $upd_sth = $dbh->prepare(
1897 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
1898         AND reservedate = ? AND (itemnumber = ? or itemnumber is NULL) "
1899         );
1900         $sth->execute( $to_biblio, 'W', 'T' );
1901         my $priority = 1;
1902         while ( my $reserve = $sth->fetchrow_hashref() ) {
1903             $upd_sth->execute(
1904                 $priority,                    $to_biblio,
1905                 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
1906                 $reserve->{'itemnumber'}
1907             );
1908             $priority++;
1909         }
1910     }
1911 }
1912
1913 =head2 RevertWaitingStatus
1914
1915   RevertWaitingStatus({ itemnumber => $itemnumber });
1916
1917   Reverts a 'waiting' hold back to a regular hold with a priority of 1.
1918
1919   Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
1920           item level hold, even if it was only a bibliolevel hold to
1921           begin with. This is because we can no longer know if a hold
1922           was item-level or bib-level after a hold has been set to
1923           waiting status.
1924
1925 =cut
1926
1927 sub RevertWaitingStatus {
1928     my ( $params ) = @_;
1929     my $itemnumber = $params->{'itemnumber'};
1930
1931     return unless ( $itemnumber );
1932
1933     my $dbh = C4::Context->dbh;
1934
1935     ## Get the waiting reserve we want to revert
1936     my $query = "
1937         SELECT * FROM reserves
1938         WHERE itemnumber = ?
1939         AND found IS NOT NULL
1940     ";
1941     my $sth = $dbh->prepare( $query );
1942     $sth->execute( $itemnumber );
1943     my $reserve = $sth->fetchrow_hashref();
1944
1945     my $hold = Koha::Holds->find( $reserve->{reserve_id} ); # TODO Remove the next raw SQL statements and use this instead
1946
1947     ## Increment the priority of all other non-waiting
1948     ## reserves for this bib record
1949     $query = "
1950         UPDATE reserves
1951         SET
1952           priority = priority + 1
1953         WHERE
1954           biblionumber =  ?
1955         AND
1956           priority > 0
1957     ";
1958     $sth = $dbh->prepare( $query );
1959     $sth->execute( $reserve->{'biblionumber'} );
1960
1961     $hold->set(
1962         {
1963             priority    => 1,
1964             found       => undef,
1965             waitingdate => undef,
1966             itemnumber  => $hold->item_level_hold ? $hold->itemnumber : undef,
1967         }
1968     )->store();
1969
1970     _FixPriority( { biblionumber => $reserve->{biblionumber} } );
1971
1972     return $hold;
1973 }
1974
1975 =head2 ReserveSlip
1976
1977 ReserveSlip(
1978     {
1979         branchcode     => $branchcode,
1980         borrowernumber => $borrowernumber,
1981         biblionumber   => $biblionumber,
1982         [ itemnumber   => $itemnumber, ]
1983         [ barcode      => $barcode, ]
1984     }
1985   )
1986
1987 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
1988
1989 The letter code will be HOLD_SLIP, and the following tables are
1990 available within the slip:
1991
1992     reserves
1993     branches
1994     borrowers
1995     biblio
1996     biblioitems
1997     items
1998
1999 =cut
2000
2001 sub ReserveSlip {
2002     my ($args) = @_;
2003     my $branchcode     = $args->{branchcode};
2004     my $borrowernumber = $args->{borrowernumber};
2005     my $biblionumber   = $args->{biblionumber};
2006     my $itemnumber     = $args->{itemnumber};
2007     my $barcode        = $args->{barcode};
2008
2009
2010     my $patron = Koha::Patrons->find($borrowernumber);
2011
2012     my $hold;
2013     if ($itemnumber || $barcode ) {
2014         $itemnumber ||= Koha::Items->find( { barcode => $barcode } )->itemnumber;
2015
2016         $hold = Koha::Holds->search(
2017             {
2018                 biblionumber   => $biblionumber,
2019                 borrowernumber => $borrowernumber,
2020                 itemnumber     => $itemnumber
2021             }
2022         )->next;
2023     }
2024     else {
2025         $hold = Koha::Holds->search(
2026             {
2027                 biblionumber   => $biblionumber,
2028                 borrowernumber => $borrowernumber
2029             }
2030         )->next;
2031     }
2032
2033     return unless $hold;
2034     my $reserve = $hold->unblessed;
2035
2036     return  C4::Letters::GetPreparedLetter (
2037         module => 'circulation',
2038         letter_code => 'HOLD_SLIP',
2039         branchcode => $branchcode,
2040         lang => $patron->lang,
2041         tables => {
2042             'reserves'    => $reserve,
2043             'branches'    => $reserve->{branchcode},
2044             'borrowers'   => $reserve->{borrowernumber},
2045             'biblio'      => $reserve->{biblionumber},
2046             'biblioitems' => $reserve->{biblionumber},
2047             'items'       => $reserve->{itemnumber},
2048         },
2049     );
2050 }
2051
2052 =head2 GetReservesControlBranch
2053
2054   my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
2055
2056   Return the branchcode to be used to determine which reserves
2057   policy applies to a transaction.
2058
2059   C<$item> is a hashref for an item. Only 'homebranch' is used.
2060
2061   C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
2062
2063 =cut
2064
2065 sub GetReservesControlBranch {
2066     my ( $item, $borrower ) = @_;
2067
2068     my $reserves_control = C4::Context->preference('ReservesControlBranch');
2069
2070     my $branchcode =
2071         ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
2072       : ( $reserves_control eq 'PatronLibrary' )   ? $borrower->{'branchcode'}
2073       :                                              undef;
2074
2075     return $branchcode;
2076 }
2077
2078 =head2 CalculatePriority
2079
2080     my $p = CalculatePriority($biblionumber, $resdate);
2081
2082 Calculate priority for a new reserve on biblionumber, placing it at
2083 the end of the line of all holds whose start date falls before
2084 the current system time and that are neither on the hold shelf
2085 or in transit.
2086
2087 The reserve date parameter is optional; if it is supplied, the
2088 priority is based on the set of holds whose start date falls before
2089 the parameter value.
2090
2091 After calculation of this priority, it is recommended to call
2092 _ShiftPriorityByDateAndPriority. Note that this is currently done in
2093 AddReserves.
2094
2095 =cut
2096
2097 sub CalculatePriority {
2098     my ( $biblionumber, $resdate ) = @_;
2099
2100     my $sql = q{
2101         SELECT COUNT(*) FROM reserves
2102         WHERE biblionumber = ?
2103         AND   priority > 0
2104         AND   (found IS NULL OR found = '')
2105     };
2106     #skip found==W or found==T (waiting or transit holds)
2107     if( $resdate ) {
2108         $sql.= ' AND ( reservedate <= ? )';
2109     }
2110     else {
2111         $sql.= ' AND ( reservedate < NOW() )';
2112     }
2113     my $dbh = C4::Context->dbh();
2114     my @row = $dbh->selectrow_array(
2115         $sql,
2116         undef,
2117         $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2118     );
2119
2120     return @row ? $row[0]+1 : 1;
2121 }
2122
2123 =head2 IsItemOnHoldAndFound
2124
2125     my $bool = IsItemFoundHold( $itemnumber );
2126
2127     Returns true if the item is currently on hold
2128     and that hold has a non-null found status ( W, T, etc. )
2129
2130 =cut
2131
2132 sub IsItemOnHoldAndFound {
2133     my ($itemnumber) = @_;
2134
2135     my $rs = Koha::Database->new()->schema()->resultset('Reserve');
2136
2137     my $found = $rs->count(
2138         {
2139             itemnumber => $itemnumber,
2140             found      => { '!=' => undef }
2141         }
2142     );
2143
2144     return $found;
2145 }
2146
2147 =head2 GetMaxPatronHoldsForRecord
2148
2149 my $holds_per_record = ReservesControlBranch( $borrowernumber, $biblionumber );
2150
2151 For multiple holds on a given record for a given patron, the max
2152 number of record level holds that a patron can be placed is the highest
2153 value of the holds_per_record rule for each item if the record for that
2154 patron. This subroutine finds and returns the highest holds_per_record
2155 rule value for a given patron id and record id.
2156
2157 =cut
2158
2159 sub GetMaxPatronHoldsForRecord {
2160     my ( $borrowernumber, $biblionumber ) = @_;
2161
2162     my $patron = Koha::Patrons->find($borrowernumber);
2163     my @items = Koha::Items->search( { biblionumber => $biblionumber } );
2164
2165     my $controlbranch = C4::Context->preference('ReservesControlBranch');
2166
2167     my $categorycode = $patron->categorycode;
2168     my $branchcode;
2169     $branchcode = $patron->branchcode if ( $controlbranch eq "PatronLibrary" );
2170
2171     my $max = 0;
2172     foreach my $item (@items) {
2173         my $itemtype = $item->effective_itemtype();
2174
2175         $branchcode = $item->homebranch if ( $controlbranch eq "ItemHomeLibrary" );
2176
2177         my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2178         my $holds_per_record = $rule ? $rule->{holds_per_record} : 0;
2179         $max = $holds_per_record if $holds_per_record > $max;
2180     }
2181
2182     return $max;
2183 }
2184
2185 =head2 GetHoldRule
2186
2187 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2188
2189 Returns the matching hold related issuingrule fields for a given
2190 patron category, itemtype, and library.
2191
2192 =cut
2193
2194 sub GetHoldRule {
2195     my ( $categorycode, $itemtype, $branchcode ) = @_;
2196
2197     my $dbh = C4::Context->dbh;
2198
2199     my $sth = $dbh->prepare(
2200         q{
2201          SELECT categorycode, itemtype, branchcode, reservesallowed, holds_per_record, holds_per_day
2202            FROM issuingrules
2203           WHERE (categorycode in (?,'*') )
2204             AND (itemtype IN (?,'*'))
2205             AND (branchcode IN (?,'*'))
2206        ORDER BY categorycode DESC,
2207                 itemtype     DESC,
2208                 branchcode   DESC
2209         }
2210     );
2211
2212     $sth->execute( $categorycode, $itemtype, $branchcode );
2213
2214     return $sth->fetchrow_hashref();
2215 }
2216
2217 =head1 AUTHOR
2218
2219 Koha Development Team <http://koha-community.org/>
2220
2221 =cut
2222
2223 1;