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