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