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