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