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