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