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