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