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