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