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