Bug 19809: Re-allow to call Koha::Objects::find in list context
[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::IssuingRules;
44 use Koha::ItemTypes;
45 use Koha::Items;
46 use Koha::Libraries;
47 use Koha::Old::Hold;
48 use Koha::Patrons;
49
50 use Carp;
51 use Data::Dumper;
52 use List::MoreUtils qw( firstidx any );
53
54 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
55
56 =head1 NAME
57
58 C4::Reserves - Koha functions for dealing with reservation.
59
60 =head1 SYNOPSIS
61
62   use C4::Reserves;
63
64 =head1 DESCRIPTION
65
66 This modules provides somes functions to deal with reservations.
67
68   Reserves are stored in reserves table.
69   The following columns contains important values :
70   - priority >0      : then the reserve is at 1st stage, and not yet affected to any item.
71              =0      : then the reserve is being dealed
72   - found : NULL       : means the patron requested the 1st available, and we haven't chosen the item
73             T(ransit)  : the reserve is linked to an item but is in transit to the pickup branch
74             W(aiting)  : the reserve is linked to an item, is at the pickup branch, and is waiting on the hold shelf
75             F(inished) : the reserve has been completed, and is done
76   - itemnumber : empty : the reserve is still unaffected to an item
77                  filled: the reserve is attached to an item
78   The complete workflow is :
79   ==== 1st use case ====
80   patron request a document, 1st available :                      P >0, F=NULL, I=NULL
81   a library having it run "transfertodo", and clic on the list
82          if there is no transfer to do, the reserve waiting
83          patron can pick it up                                    P =0, F=W,    I=filled
84          if there is a transfer to do, write in branchtransfer    P =0, F=T,    I=filled
85            The pickup library receive the book, it check in       P =0, F=W,    I=filled
86   The patron borrow the book                                      P =0, F=F,    I=filled
87
88   ==== 2nd use case ====
89   patron requests a document, a given item,
90     If pickup is holding branch                                   P =0, F=W,   I=filled
91     If transfer needed, write in branchtransfer                   P =0, F=T,    I=filled
92         The pickup library receive the book, it checks it in      P =0, F=W,    I=filled
93   The patron borrow the book                                      P =0, F=F,    I=filled
94
95 =head1 FUNCTIONS
96
97 =cut
98
99 BEGIN {
100     require Exporter;
101     @ISA = qw(Exporter);
102     @EXPORT = qw(
103         &AddReserve
104
105         &GetReserveStatus
106
107         &GetOtherReserves
108
109         &ModReserveFill
110         &ModReserveAffect
111         &ModReserve
112         &ModReserveStatus
113         &ModReserveCancelAll
114         &ModReserveMinusPriority
115         &MoveReserve
116
117         &CheckReserves
118         &CanBookBeReserved
119         &CanItemBeReserved
120         &CanReserveBeCanceledFromOpac
121         &CancelExpiredReserves
122
123         &AutoUnsuspendReserves
124
125         &IsAvailableForItemLevelRequest
126
127         &AlterPriority
128         &ToggleLowestPriority
129
130         &ReserveSlip
131         &ToggleSuspend
132         &SuspendAll
133
134         &GetReservesControlBranch
135
136         IsItemOnHoldAndFound
137
138         GetMaxPatronHoldsForRecord
139     );
140     @EXPORT_OK = qw( MergeHolds );
141 }
142
143 =head2 AddReserve
144
145     AddReserve($branch,$borrowernumber,$biblionumber,$bibitems,$priority,$resdate,$expdate,$notes,$title,$checkitem,$found)
146
147 Adds reserve and generates HOLDPLACED message.
148
149 The following tables are available witin the HOLDPLACED message:
150
151     branches
152     borrowers
153     biblio
154     biblioitems
155     items
156     reserves
157
158 =cut
159
160 sub AddReserve {
161     my (
162         $branch,   $borrowernumber, $biblionumber, $bibitems,
163         $priority, $resdate,        $expdate,      $notes,
164         $title,    $checkitem,      $found,        $itemtype
165     ) = @_;
166
167     $resdate = output_pref( { str => dt_from_string( $resdate ), dateonly => 1, dateformat => 'iso' })
168         or output_pref({ dt => dt_from_string, dateonly => 1, dateformat => 'iso' });
169
170     $expdate = output_pref({ str => $expdate, dateonly => 1, dateformat => 'iso' });
171
172     # if we have an item selectionned, and the pickup branch is the same as the holdingbranch
173     # of the document, we force the value $priority and $found .
174     if ( $checkitem and not C4::Context->preference('ReservesNeedReturns') ) {
175         my $item = Koha::Items->find( $checkitem ); # FIXME Prevent bad calls
176
177         if (
178             # If item is already checked out, it cannot be set waiting
179             !$item->onloan
180
181             # The item can't be waiting if it needs a transfer
182             && $item->holdingbranch eq $branch
183
184             # Similarly, if in transit it can't be waiting
185             && !$item->get_transfer
186
187             # If we can't hold damaged items, and it is damaged, it can't be waiting
188             && ( $item->damaged && C4::Context->preference('AllowHoldsOnDamagedItems') || !$item->damaged )
189
190             # Lastly, if this already has holds, we shouldn't make it waiting for the new hold
191             && !$item->current_holds->count )
192         {
193             $priority = 0;
194             $found = 'W';
195         }
196     }
197
198     if ( C4::Context->preference('AllowHoldDateInFuture') ) {
199
200         # Make room in reserves for this before those of a later reserve date
201         $priority = _ShiftPriorityByDateAndPriority( $biblionumber, $resdate, $priority );
202     }
203
204     my $waitingdate;
205
206     # If the reserv had the waiting status, we had the value of the resdate
207     if ( $found && $found eq 'W' ) {
208         $waitingdate = $resdate;
209     }
210
211     # Don't add itemtype limit if specific item is selected
212     $itemtype = undef if $checkitem;
213
214     # updates take place here
215     my $hold = Koha::Hold->new(
216         {
217             borrowernumber => $borrowernumber,
218             biblionumber   => $biblionumber,
219             reservedate    => $resdate,
220             branchcode     => $branch,
221             priority       => $priority,
222             reservenotes   => $notes,
223             itemnumber     => $checkitem,
224             found          => $found,
225             waitingdate    => $waitingdate,
226             expirationdate => $expdate,
227             itemtype       => $itemtype,
228             item_level_hold => $checkitem ? 1 : 0,
229         }
230     )->store();
231     $hold->set_waiting() if $found && $found eq 'W';
232
233     logaction( 'HOLDS', 'CREATE', $hold->id, Dumper($hold->unblessed) )
234         if C4::Context->preference('HoldsLog');
235
236     my $reserve_id = $hold->id();
237
238     # add a reserve fee if needed
239     if ( C4::Context->preference('HoldFeeMode') ne 'any_time_is_collected' ) {
240         my $reserve_fee = GetReserveFee( $borrowernumber, $biblionumber );
241         ChargeReserveFee( $borrowernumber, $reserve_fee, $title );
242     }
243
244     _FixPriority({ biblionumber => $biblionumber});
245
246     # Send e-mail to librarian if syspref is active
247     if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){
248         my $patron = Koha::Patrons->find( $borrowernumber );
249         my $library = $patron->library;
250         if ( my $letter =  C4::Letters::GetPreparedLetter (
251             module => 'reserves',
252             letter_code => 'HOLDPLACED',
253             branchcode => $branch,
254             lang => $patron->lang,
255             tables => {
256                 'branches'    => $library->unblessed,
257                 'borrowers'   => $patron->unblessed,
258                 'biblio'      => $biblionumber,
259                 'biblioitems' => $biblionumber,
260                 'items'       => $checkitem,
261                 'reserves'    => $hold->unblessed,
262             },
263         ) ) {
264
265             my $admin_email_address = $library->branchemail || C4::Context->preference('KohaAdminEmailAddress');
266
267             C4::Letters::EnqueueLetter(
268                 {   letter                 => $letter,
269                     borrowernumber         => $borrowernumber,
270                     message_transport_type => 'email',
271                     from_address           => $admin_email_address,
272                     to_address           => $admin_email_address,
273                 }
274             );
275         }
276     }
277
278     return $reserve_id;
279 }
280
281 =head2 CanBookBeReserved
282
283   $canReserve = &CanBookBeReserved($borrowernumber, $biblionumber, $branchcode)
284   if ($canReserve eq 'OK') { #We can reserve this Item! }
285
286 See CanItemBeReserved() for possible return values.
287
288 =cut
289
290 sub CanBookBeReserved{
291     my ($borrowernumber, $biblionumber, $pickup_branchcode) = @_;
292
293     my @itemnumbers = Koha::Items->search({ biblionumber => $biblionumber})->get_column("itemnumber");
294     #get items linked via host records
295     my @hostitems = get_hostitemnumbers_of($biblionumber);
296     if (@hostitems){
297         push (@itemnumbers, @hostitems);
298     }
299
300     my $canReserve = { status => '' };
301     foreach my $itemnumber (@itemnumbers) {
302         $canReserve = CanItemBeReserved( $borrowernumber, $itemnumber, $pickup_branchcode );
303         return { status => 'OK' } if $canReserve->{status} eq 'OK';
304     }
305     return $canReserve;
306 }
307
308 =head2 CanItemBeReserved
309
310   $canReserve = &CanItemBeReserved($borrowernumber, $itemnumber, $branchcode)
311   if ($canReserve->{status} eq 'OK') { #We can reserve this Item! }
312
313 @RETURNS { status => OK },              if the Item can be reserved.
314          { status => ageRestricted },   if the Item is age restricted for this borrower.
315          { status => damaged },         if the Item is damaged.
316          { status => cannotReserveFromOtherBranches }, if syspref 'canreservefromotherbranches' is OK.
317          { status => branchNotInHoldGroup }, if borrower home library is not in hold group, and holds are only allowed from hold groups.
318          { status => tooManyReserves, limit => $limit }, if the borrower has exceeded their maximum reserve amount.
319          { status => notReservable },   if holds on this item are not allowed
320          { status => libraryNotFound },   if given branchcode is not an existing library
321          { status => libraryNotPickupLocation },   if given branchcode is not configured to be a pickup location
322          { status => cannotBeTransferred }, if branch transfer limit applies on given item and branchcode
323          { status => pickupNotInHoldGroup }, pickup location is not in hold group, and pickup locations are only allowed from hold groups.
324
325 =cut
326
327 sub CanItemBeReserved {
328     my ( $borrowernumber, $itemnumber, $pickup_branchcode ) = @_;
329
330     my $dbh = C4::Context->dbh;
331     my $ruleitemtype;    # itemtype of the matching issuing rule
332     my $allowedreserves  = 0; # Total number of holds allowed across all records
333     my $holds_per_record = 1; # Total number of holds allowed for this one given record
334     my $holds_per_day;        # Default to unlimited
335
336     # we retrieve borrowers and items informations #
337     # item->{itype} will come for biblioitems if necessery
338     my $item       = Koha::Items->find($itemnumber);
339     my $biblio     = $item->biblio;
340     my $patron = Koha::Patrons->find( $borrowernumber );
341     my $borrower = $patron->unblessed;
342
343     # If an item is damaged and we don't allow holds on damaged items, we can stop right here
344     return { status =>'damaged' }
345       if ( $item->damaged
346         && !C4::Context->preference('AllowHoldsOnDamagedItems') );
347
348     # Check for the age restriction
349     my ( $ageRestriction, $daysToAgeRestriction ) =
350       C4::Circulation::GetAgeRestriction( $biblio->biblioitem->agerestriction, $borrower );
351     return { status => 'ageRestricted' } if $daysToAgeRestriction && $daysToAgeRestriction > 0;
352
353     # Check that the patron doesn't have an item level hold on this item already
354     return { status =>'itemAlreadyOnHold' }
355       if Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->count();
356
357     my $controlbranch = C4::Context->preference('ReservesControlBranch');
358
359     my $querycount = q{
360         SELECT count(*) AS count
361           FROM reserves
362      LEFT JOIN items USING (itemnumber)
363      LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber)
364      LEFT JOIN borrowers USING (borrowernumber)
365          WHERE borrowernumber = ?
366     };
367
368     my $branchcode  = "";
369     my $branchfield = "reserves.branchcode";
370
371     if ( $controlbranch eq "ItemHomeLibrary" ) {
372         $branchfield = "items.homebranch";
373         $branchcode  = $item->homebranch;
374     }
375     elsif ( $controlbranch eq "PatronLibrary" ) {
376         $branchfield = "borrowers.branchcode";
377         $branchcode  = $borrower->{branchcode};
378     }
379
380     # we retrieve rights
381     if ( my $rights = GetHoldRule( $borrower->{'categorycode'}, $item->effective_itemtype, $branchcode ) ) {
382         $ruleitemtype     = $rights->{itemtype};
383         $allowedreserves  = $rights->{reservesallowed};
384         $holds_per_record = $rights->{holds_per_record};
385         $holds_per_day    = $rights->{holds_per_day};
386     }
387     else {
388         $ruleitemtype = '*';
389     }
390
391     my $holds = Koha::Holds->search(
392         {
393             borrowernumber => $borrowernumber,
394             biblionumber   => $item->biblionumber,
395         }
396     );
397     if ( $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 &&
407           (   ( $holds_per_day > 0 && $today_holds->count() >= $holds_per_day )
408            or ( $holds_per_day == 0 ) )
409         )  {
410         return { status => 'tooManyReservesToday', limit => $holds_per_day };
411     }
412
413     # we retrieve count
414
415     $querycount .= "AND ( $branchfield = ? OR $branchfield IS NULL )";
416
417     # If using item-level itypes, fall back to the record
418     # level itemtype if the hold has no associated item
419     $querycount .=
420       C4::Context->preference('item-level_itypes')
421       ? " AND COALESCE( items.itype, biblioitems.itemtype ) = ?"
422       : " AND biblioitems.itemtype = ?"
423       if ( $ruleitemtype ne "*" );
424
425     my $sthcount = $dbh->prepare($querycount);
426
427     if ( $ruleitemtype eq "*" ) {
428         $sthcount->execute( $borrowernumber, $branchcode );
429     }
430     else {
431         $sthcount->execute( $borrowernumber, $branchcode, $ruleitemtype );
432     }
433
434     my $reservecount = "0";
435     if ( my $rowcount = $sthcount->fetchrow_hashref() ) {
436         $reservecount = $rowcount->{count};
437     }
438
439     # we check if it's ok or not
440     if ( $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'} == $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::IssuingRules->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     my $lookahead = C4::Context->preference('ConfirmFutureHolds'); #number of days to look for future holds
1840     my ( $restype, $res, undef ) = CheckReserves( $itemnumber, undef, $lookahead );
1841     return unless $res;
1842
1843     my $biblionumber     =  $res->{biblionumber};
1844
1845     if ($res->{borrowernumber} == $borrowernumber) {
1846         ModReserveFill($res);
1847     }
1848     else {
1849         # warn "Reserved";
1850         # The item is reserved by someone else.
1851         # Find this item in the reserves
1852
1853         my $borr_res  = Koha::Holds->search({
1854             borrowernumber => $borrowernumber,
1855             biblionumber   => $biblionumber,
1856         },{
1857             order_by       => 'priority'
1858         })->next();
1859
1860         if ( $borr_res ) {
1861             # The item is reserved by the current patron
1862             ModReserveFill($borr_res->unblessed);
1863         }
1864
1865         if ( $cancelreserve eq 'revert' ) { ## Revert waiting reserve to priority 1
1866             RevertWaitingStatus({ itemnumber => $itemnumber });
1867         }
1868         elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item
1869             my $hold = Koha::Holds->find( $res->{reserve_id} );
1870             $hold->cancel;
1871         }
1872     }
1873 }
1874
1875 =head2 MergeHolds
1876
1877   MergeHolds($dbh,$to_biblio, $from_biblio);
1878
1879 This shifts the holds from C<$from_biblio> to C<$to_biblio> and reorders them by the date they were placed
1880
1881 =cut
1882
1883 sub MergeHolds {
1884     my ( $dbh, $to_biblio, $from_biblio ) = @_;
1885     my $sth = $dbh->prepare(
1886         "SELECT count(*) as reserve_count FROM reserves WHERE biblionumber = ?"
1887     );
1888     $sth->execute($from_biblio);
1889     if ( my $data = $sth->fetchrow_hashref() ) {
1890
1891         # holds exist on old record, if not we don't need to do anything
1892         $sth = $dbh->prepare(
1893             "UPDATE reserves SET biblionumber = ? WHERE biblionumber = ?");
1894         $sth->execute( $to_biblio, $from_biblio );
1895
1896         # Reorder by date
1897         # don't reorder those already waiting
1898
1899         $sth = $dbh->prepare(
1900 "SELECT * FROM reserves WHERE biblionumber = ? AND (found <> ? AND found <> ? OR found is NULL) ORDER BY reservedate ASC"
1901         );
1902         my $upd_sth = $dbh->prepare(
1903 "UPDATE reserves SET priority = ? WHERE biblionumber = ? AND borrowernumber = ?
1904         AND reservedate = ? AND (itemnumber = ? or itemnumber is NULL) "
1905         );
1906         $sth->execute( $to_biblio, 'W', 'T' );
1907         my $priority = 1;
1908         while ( my $reserve = $sth->fetchrow_hashref() ) {
1909             $upd_sth->execute(
1910                 $priority,                    $to_biblio,
1911                 $reserve->{'borrowernumber'}, $reserve->{'reservedate'},
1912                 $reserve->{'itemnumber'}
1913             );
1914             $priority++;
1915         }
1916     }
1917 }
1918
1919 =head2 RevertWaitingStatus
1920
1921   RevertWaitingStatus({ itemnumber => $itemnumber });
1922
1923   Reverts a 'waiting' hold back to a regular hold with a priority of 1.
1924
1925   Caveat: Any waiting hold fixed with RevertWaitingStatus will be an
1926           item level hold, even if it was only a bibliolevel hold to
1927           begin with. This is because we can no longer know if a hold
1928           was item-level or bib-level after a hold has been set to
1929           waiting status.
1930
1931 =cut
1932
1933 sub RevertWaitingStatus {
1934     my ( $params ) = @_;
1935     my $itemnumber = $params->{'itemnumber'};
1936
1937     return unless ( $itemnumber );
1938
1939     my $dbh = C4::Context->dbh;
1940
1941     ## Get the waiting reserve we want to revert
1942     my $query = "
1943         SELECT * FROM reserves
1944         WHERE itemnumber = ?
1945         AND found IS NOT NULL
1946     ";
1947     my $sth = $dbh->prepare( $query );
1948     $sth->execute( $itemnumber );
1949     my $reserve = $sth->fetchrow_hashref();
1950
1951     my $hold = Koha::Holds->find( $reserve->{reserve_id} ); # TODO Remove the next raw SQL statements and use this instead
1952
1953     ## Increment the priority of all other non-waiting
1954     ## reserves for this bib record
1955     $query = "
1956         UPDATE reserves
1957         SET
1958           priority = priority + 1
1959         WHERE
1960           biblionumber =  ?
1961         AND
1962           priority > 0
1963     ";
1964     $sth = $dbh->prepare( $query );
1965     $sth->execute( $reserve->{'biblionumber'} );
1966
1967     ## Fix up the currently waiting reserve
1968     $query = "
1969     UPDATE reserves
1970     SET
1971       priority = 1,
1972       found = NULL,
1973       waitingdate = NULL
1974     WHERE
1975       reserve_id = ?
1976     ";
1977     $sth = $dbh->prepare( $query );
1978     $sth->execute( $reserve->{'reserve_id'} );
1979
1980     unless ( $hold->item_level_hold ) {
1981         $hold->itemnumber(undef)->store;
1982     }
1983
1984     _FixPriority( { biblionumber => $reserve->{biblionumber} } );
1985 }
1986
1987 =head2 ReserveSlip
1988
1989 ReserveSlip(
1990     {
1991         branchcode     => $branchcode,
1992         borrowernumber => $borrowernumber,
1993         biblionumber   => $biblionumber,
1994         [ itemnumber   => $itemnumber, ]
1995         [ barcode      => $barcode, ]
1996     }
1997   )
1998
1999 Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef
2000
2001 The letter code will be HOLD_SLIP, and the following tables are
2002 available within the slip:
2003
2004     reserves
2005     branches
2006     borrowers
2007     biblio
2008     biblioitems
2009     items
2010
2011 =cut
2012
2013 sub ReserveSlip {
2014     my ($args) = @_;
2015     my $branchcode     = $args->{branchcode};
2016     my $borrowernumber = $args->{borrowernumber};
2017     my $biblionumber   = $args->{biblionumber};
2018     my $itemnumber     = $args->{itemnumber};
2019     my $barcode        = $args->{barcode};
2020
2021
2022     my $patron = Koha::Patrons->find($borrowernumber);
2023
2024     my $hold;
2025     if ($itemnumber || $barcode ) {
2026         $itemnumber ||= Koha::Items->find( { barcode => $barcode } )->itemnumber;
2027
2028         $hold = Koha::Holds->search(
2029             {
2030                 biblionumber   => $biblionumber,
2031                 borrowernumber => $borrowernumber,
2032                 itemnumber     => $itemnumber
2033             }
2034         )->next;
2035     }
2036     else {
2037         $hold = Koha::Holds->search(
2038             {
2039                 biblionumber   => $biblionumber,
2040                 borrowernumber => $borrowernumber
2041             }
2042         )->next;
2043     }
2044
2045     return unless $hold;
2046     my $reserve = $hold->unblessed;
2047
2048     return  C4::Letters::GetPreparedLetter (
2049         module => 'circulation',
2050         letter_code => 'HOLD_SLIP',
2051         branchcode => $branchcode,
2052         lang => $patron->lang,
2053         tables => {
2054             'reserves'    => $reserve,
2055             'branches'    => $reserve->{branchcode},
2056             'borrowers'   => $reserve->{borrowernumber},
2057             'biblio'      => $reserve->{biblionumber},
2058             'biblioitems' => $reserve->{biblionumber},
2059             'items'       => $reserve->{itemnumber},
2060         },
2061     );
2062 }
2063
2064 =head2 GetReservesControlBranch
2065
2066   my $reserves_control_branch = GetReservesControlBranch($item, $borrower);
2067
2068   Return the branchcode to be used to determine which reserves
2069   policy applies to a transaction.
2070
2071   C<$item> is a hashref for an item. Only 'homebranch' is used.
2072
2073   C<$borrower> is a hashref to borrower. Only 'branchcode' is used.
2074
2075 =cut
2076
2077 sub GetReservesControlBranch {
2078     my ( $item, $borrower ) = @_;
2079
2080     my $reserves_control = C4::Context->preference('ReservesControlBranch');
2081
2082     my $branchcode =
2083         ( $reserves_control eq 'ItemHomeLibrary' ) ? $item->{'homebranch'}
2084       : ( $reserves_control eq 'PatronLibrary' )   ? $borrower->{'branchcode'}
2085       :                                              undef;
2086
2087     return $branchcode;
2088 }
2089
2090 =head2 CalculatePriority
2091
2092     my $p = CalculatePriority($biblionumber, $resdate);
2093
2094 Calculate priority for a new reserve on biblionumber, placing it at
2095 the end of the line of all holds whose start date falls before
2096 the current system time and that are neither on the hold shelf
2097 or in transit.
2098
2099 The reserve date parameter is optional; if it is supplied, the
2100 priority is based on the set of holds whose start date falls before
2101 the parameter value.
2102
2103 After calculation of this priority, it is recommended to call
2104 _ShiftPriorityByDateAndPriority. Note that this is currently done in
2105 AddReserves.
2106
2107 =cut
2108
2109 sub CalculatePriority {
2110     my ( $biblionumber, $resdate ) = @_;
2111
2112     my $sql = q{
2113         SELECT COUNT(*) FROM reserves
2114         WHERE biblionumber = ?
2115         AND   priority > 0
2116         AND   (found IS NULL OR found = '')
2117     };
2118     #skip found==W or found==T (waiting or transit holds)
2119     if( $resdate ) {
2120         $sql.= ' AND ( reservedate <= ? )';
2121     }
2122     else {
2123         $sql.= ' AND ( reservedate < NOW() )';
2124     }
2125     my $dbh = C4::Context->dbh();
2126     my @row = $dbh->selectrow_array(
2127         $sql,
2128         undef,
2129         $resdate ? ($biblionumber, $resdate) : ($biblionumber)
2130     );
2131
2132     return @row ? $row[0]+1 : 1;
2133 }
2134
2135 =head2 IsItemOnHoldAndFound
2136
2137     my $bool = IsItemFoundHold( $itemnumber );
2138
2139     Returns true if the item is currently on hold
2140     and that hold has a non-null found status ( W, T, etc. )
2141
2142 =cut
2143
2144 sub IsItemOnHoldAndFound {
2145     my ($itemnumber) = @_;
2146
2147     my $rs = Koha::Database->new()->schema()->resultset('Reserve');
2148
2149     my $found = $rs->count(
2150         {
2151             itemnumber => $itemnumber,
2152             found      => { '!=' => undef }
2153         }
2154     );
2155
2156     return $found;
2157 }
2158
2159 =head2 GetMaxPatronHoldsForRecord
2160
2161 my $holds_per_record = ReservesControlBranch( $borrowernumber, $biblionumber );
2162
2163 For multiple holds on a given record for a given patron, the max
2164 number of record level holds that a patron can be placed is the highest
2165 value of the holds_per_record rule for each item if the record for that
2166 patron. This subroutine finds and returns the highest holds_per_record
2167 rule value for a given patron id and record id.
2168
2169 =cut
2170
2171 sub GetMaxPatronHoldsForRecord {
2172     my ( $borrowernumber, $biblionumber ) = @_;
2173
2174     my $patron = Koha::Patrons->find($borrowernumber);
2175     my @items = Koha::Items->search( { biblionumber => $biblionumber } );
2176
2177     my $controlbranch = C4::Context->preference('ReservesControlBranch');
2178
2179     my $categorycode = $patron->categorycode;
2180     my $branchcode;
2181     $branchcode = $patron->branchcode if ( $controlbranch eq "PatronLibrary" );
2182
2183     my $max = 0;
2184     foreach my $item (@items) {
2185         my $itemtype = $item->effective_itemtype();
2186
2187         $branchcode = $item->homebranch if ( $controlbranch eq "ItemHomeLibrary" );
2188
2189         my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2190         my $holds_per_record = $rule ? $rule->{holds_per_record} : 0;
2191         $max = $holds_per_record if $holds_per_record > $max;
2192     }
2193
2194     return $max;
2195 }
2196
2197 =head2 GetHoldRule
2198
2199 my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode );
2200
2201 Returns the matching hold related issuingrule fields for a given
2202 patron category, itemtype, and library.
2203
2204 =cut
2205
2206 sub GetHoldRule {
2207     my ( $categorycode, $itemtype, $branchcode ) = @_;
2208
2209     my $dbh = C4::Context->dbh;
2210
2211     my $sth = $dbh->prepare(
2212         q{
2213          SELECT categorycode, itemtype, branchcode, reservesallowed, holds_per_record, holds_per_day
2214            FROM issuingrules
2215           WHERE (categorycode in (?,'*') )
2216             AND (itemtype IN (?,'*'))
2217             AND (branchcode IN (?,'*'))
2218        ORDER BY categorycode DESC,
2219                 itemtype     DESC,
2220                 branchcode   DESC
2221         }
2222     );
2223
2224     $sth->execute( $categorycode, $itemtype, $branchcode );
2225
2226     return $sth->fetchrow_hashref();
2227 }
2228
2229 =head1 AUTHOR
2230
2231 Koha Development Team <http://koha-community.org/>
2232
2233 =cut
2234
2235 1;