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