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