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