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