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