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