Bug 19057: Move C4::Reserve::GetReserve to Koha::Holds
[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 = Koha::Holds->find($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 $hold = Koha::Holds->find( $reserve_id );
878     return unless $hold;
879
880     logaction( 'HOLDS', 'CANCEL', $hold->reserve_id, Dumper($hold->unblessed) )
881         if C4::Context->preference('HoldsLog');
882
883     my $query = "
884         UPDATE reserves
885         SET    cancellationdate = now(),
886                priority         = 0
887         WHERE  reserve_id = ?
888     ";
889     my $sth = $dbh->prepare($query);
890     $sth->execute( $reserve_id );
891
892     $query = "
893         INSERT INTO old_reserves
894         SELECT * FROM reserves
895         WHERE  reserve_id = ?
896     ";
897     $sth = $dbh->prepare($query);
898     $sth->execute( $reserve_id );
899
900     $query = "
901         DELETE FROM reserves
902         WHERE  reserve_id = ?
903     ";
904     $sth = $dbh->prepare($query);
905     $sth->execute( $reserve_id );
906
907     # now fix the priority on the others....
908     _FixPriority({ biblionumber => $hold->biblionumber });
909
910     # and, if desired, charge a cancel fee
911     my $charge = C4::Context->preference("ExpireReservesMaxPickUpDelayCharge");
912     if ( $charge && $params->{'charge_cancel_fee'} ) {
913         manualinvoice($hold->borrowernumber, $hold->itemnumber, '', 'HE', $charge);
914     }
915
916     return $hold->unblessed;
917 }
918
919 =head2 ModReserve
920
921   ModReserve({ rank => $rank,
922                reserve_id => $reserve_id,
923                branchcode => $branchcode
924                [, itemnumber => $itemnumber ]
925                [, biblionumber => $biblionumber, $borrowernumber => $borrowernumber ]
926               });
927
928 Change a hold request's priority or cancel it.
929
930 C<$rank> specifies the effect of the change.  If C<$rank>
931 is 'W' or 'n', nothing happens.  This corresponds to leaving a
932 request alone when changing its priority in the holds queue
933 for a bib.
934
935 If C<$rank> is 'del', the hold request is cancelled.
936
937 If C<$rank> is an integer greater than zero, the priority of
938 the request is set to that value.  Since priority != 0 means
939 that the item is not waiting on the hold shelf, setting the
940 priority to a non-zero value also sets the request's found
941 status and waiting date to NULL.
942
943 The optional C<$itemnumber> parameter is used only when
944 C<$rank> is a non-zero integer; if supplied, the itemnumber
945 of the hold request is set accordingly; if omitted, the itemnumber
946 is cleared.
947
948 B<FIXME:> Note that the forgoing can have the effect of causing
949 item-level hold requests to turn into title-level requests.  This
950 will be fixed once reserves has separate columns for requested
951 itemnumber and supplying itemnumber.
952
953 =cut
954
955 sub ModReserve {
956     my ( $params ) = @_;
957
958     my $rank = $params->{'rank'};
959     my $reserve_id = $params->{'reserve_id'};
960     my $branchcode = $params->{'branchcode'};
961     my $itemnumber = $params->{'itemnumber'};
962     my $suspend_until = $params->{'suspend_until'};
963     my $borrowernumber = $params->{'borrowernumber'};
964     my $biblionumber = $params->{'biblionumber'};
965
966     return if $rank eq "W";
967     return if $rank eq "n";
968
969     return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) );
970     $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber }) unless ( $reserve_id );
971
972     if ( $rank eq "del" ) {
973         CancelReserve({ reserve_id => $reserve_id });
974     }
975     elsif ($rank =~ /^\d+/ and $rank > 0) {
976         my $hold = Koha::Holds->find($reserve_id);
977         logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->unblessed) )
978             if C4::Context->preference('HoldsLog');
979
980         $hold->set(
981             {
982                 priority    => $rank,
983                 branchcode  => $branchcode,
984                 itemnumber  => $itemnumber,
985                 found       => undef,
986                 waitingdate => undef
987             }
988         )->store();
989
990         if ( defined( $suspend_until ) ) {
991             if ( $suspend_until ) {
992                 $suspend_until = eval { dt_from_string( $suspend_until ) };
993                 $hold->suspend_hold( $suspend_until );
994             } else {
995                 # If the hold is suspended leave the hold suspended, but convert it to an indefinite hold.
996                 # If the hold is not suspended, this does nothing.
997                 $hold->set( { suspend_until => undef } )->store();
998             }
999         }
1000
1001         _FixPriority({ reserve_id => $reserve_id, rank =>$rank });
1002     }
1003 }
1004
1005 =head2 ModReserveFill
1006
1007   &ModReserveFill($reserve);
1008
1009 Fill a reserve. If I understand this correctly, this means that the
1010 reserved book has been found and given to the patron who reserved it.
1011
1012 C<$reserve> specifies the reserve to fill. It is a reference-to-hash
1013 whose keys are fields from the reserves table in the Koha database.
1014
1015 =cut
1016
1017 sub ModReserveFill {
1018     my ($res) = @_;
1019     my $reserve_id = $res->{'reserve_id'};
1020
1021     my $hold = Koha::Holds->find($reserve_id);
1022
1023     # get the priority on this record....
1024     my $priority = $hold->priority;
1025
1026     # update the hold statuses, no need to store it though, we will be deleting it anyway
1027     $hold->set(
1028         {
1029             found    => 'F',
1030             priority => 0,
1031         }
1032     );
1033
1034     Koha::Old::Hold->new( $hold->unblessed() )->store();
1035
1036     $hold->delete();
1037
1038     if ( C4::Context->preference('HoldFeeMode') eq 'any_time_is_collected' ) {
1039         my $reserve_fee = GetReserveFee( $hold->borrowernumber, $hold->biblionumber );
1040         ChargeReserveFee( $hold->borrowernumber, $reserve_fee, $hold->biblio->title );
1041     }
1042
1043     # now fix the priority on the others (if the priority wasn't
1044     # already sorted!)....
1045     unless ( $priority == 0 ) {
1046         _FixPriority( { reserve_id => $reserve_id, biblionumber => $hold->biblionumber } );
1047     }
1048 }
1049
1050 =head2 ModReserveStatus
1051
1052   &ModReserveStatus($itemnumber, $newstatus);
1053
1054 Update the reserve status for the active (priority=0) reserve.
1055
1056 $itemnumber is the itemnumber the reserve is on
1057
1058 $newstatus is the new status.
1059
1060 =cut
1061
1062 sub ModReserveStatus {
1063
1064     #first : check if we have a reservation for this item .
1065     my ($itemnumber, $newstatus) = @_;
1066     my $dbh = C4::Context->dbh;
1067
1068     my $query = "UPDATE reserves SET found = ?, waitingdate = NOW() WHERE itemnumber = ? AND found IS NULL AND priority = 0";
1069     my $sth_set = $dbh->prepare($query);
1070     $sth_set->execute( $newstatus, $itemnumber );
1071
1072     if ( C4::Context->preference("ReturnToShelvingCart") && $newstatus ) {
1073       CartToShelf( $itemnumber );
1074     }
1075 }
1076
1077 =head2 ModReserveAffect
1078
1079   &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend,$reserve_id);
1080
1081 This function affect an item and a status for a given reserve, either fetched directly
1082 by record_id, or by borrowernumber and itemnumber or biblionumber. If only biblionumber
1083 is given, only first reserve returned is affected, which is ok for anything but
1084 multi-item holds.
1085
1086 if $transferToDo is not set, then the status is set to "Waiting" as well.
1087 otherwise, a transfer is on the way, and the end of the transfer will
1088 take care of the waiting status
1089
1090 =cut
1091
1092 sub ModReserveAffect {
1093     my ( $itemnumber, $borrowernumber, $transferToDo, $reserve_id ) = @_;
1094     my $dbh = C4::Context->dbh;
1095
1096     # we want to attach $itemnumber to $borrowernumber, find the biblionumber
1097     # attached to $itemnumber
1098     my $sth = $dbh->prepare("SELECT biblionumber FROM items WHERE itemnumber=?");
1099     $sth->execute($itemnumber);
1100     my ($biblionumber) = $sth->fetchrow;
1101
1102     # get request - need to find out if item is already
1103     # waiting in order to not send duplicate hold filled notifications
1104
1105     my $hold;
1106     # Find hold by id if we have it
1107     $hold = Koha::Holds->find( $reserve_id ) if $reserve_id;
1108     # Find item level hold for this item if there is one
1109     $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->next();
1110     # Find record level hold if there is no item level hold
1111     $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->next();
1112
1113     return unless $hold;
1114
1115     my $already_on_shelf = $hold->found && $hold->found eq 'W';
1116
1117     $hold->itemnumber($itemnumber);
1118     $hold->set_waiting($transferToDo);
1119
1120     _koha_notify_reserve( $hold->reserve_id )
1121       if ( !$transferToDo && !$already_on_shelf );
1122
1123     _FixPriority( { biblionumber => $biblionumber } );
1124
1125     if ( C4::Context->preference("ReturnToShelvingCart") ) {
1126         CartToShelf($itemnumber);
1127     }
1128
1129     return;
1130 }
1131
1132 =head2 ModReserveCancelAll
1133
1134   ($messages,$nextreservinfo) = &ModReserveCancelAll($itemnumber,$borrowernumber);
1135
1136 function to cancel reserv,check other reserves, and transfer document if it's necessary
1137
1138 =cut
1139
1140 sub ModReserveCancelAll {
1141     my $messages;
1142     my $nextreservinfo;
1143     my ( $itemnumber, $borrowernumber ) = @_;
1144
1145     #step 1 : cancel the reservation
1146     my $CancelReserve = CancelReserve({ itemnumber => $itemnumber, borrowernumber => $borrowernumber });
1147
1148     #step 2 launch the subroutine of the others reserves
1149     ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber);
1150
1151     return ( $messages, $nextreservinfo );
1152 }
1153
1154 =head2 ModReserveMinusPriority
1155
1156   &ModReserveMinusPriority($itemnumber,$borrowernumber,$biblionumber)
1157
1158 Reduce the values of queued list
1159
1160 =cut
1161
1162 sub ModReserveMinusPriority {
1163     my ( $itemnumber, $reserve_id ) = @_;
1164
1165     #first step update the value of the first person on reserv
1166     my $dbh   = C4::Context->dbh;
1167     my $query = "
1168         UPDATE reserves
1169         SET    priority = 0 , itemnumber = ?
1170         WHERE  reserve_id = ?
1171     ";
1172     my $sth_upd = $dbh->prepare($query);
1173     $sth_upd->execute( $itemnumber, $reserve_id );
1174     # second step update all others reserves
1175     _FixPriority({ reserve_id => $reserve_id, rank => '0' });
1176 }
1177
1178 =head2 IsAvailableForItemLevelRequest
1179
1180   my $is_available = IsAvailableForItemLevelRequest($item_record,$borrower_record);
1181
1182 Checks whether a given item record is available for an
1183 item-level hold request.  An item is available if
1184
1185 * it is not lost AND
1186 * it is not damaged AND
1187 * it is not withdrawn AND
1188 * does not have a not for loan value > 0
1189
1190 Need to check the issuingrules onshelfholds column,
1191 if this is set items on the shelf can be placed on hold
1192
1193 Note that IsAvailableForItemLevelRequest() does not
1194 check if the staff operator is authorized to place
1195 a request on the item - in particular,
1196 this routine does not check IndependentBranches
1197 and canreservefromotherbranches.
1198
1199 =cut
1200
1201 sub IsAvailableForItemLevelRequest {
1202     my $item = shift;
1203     my $borrower = shift;
1204
1205     my $dbh = C4::Context->dbh;
1206     # must check the notforloan setting of the itemtype
1207     # FIXME - a lot of places in the code do this
1208     #         or something similar - need to be
1209     #         consolidated
1210     my $itype = _get_itype($item);
1211     my $notforloan_per_itemtype
1212       = $dbh->selectrow_array("SELECT notforloan FROM itemtypes WHERE itemtype = ?",
1213                               undef, $itype);
1214
1215     return 0 if
1216         $notforloan_per_itemtype ||
1217         $item->{itemlost}        ||
1218         $item->{notforloan} > 0  ||
1219         $item->{withdrawn}        ||
1220         ($item->{damaged} && !C4::Context->preference('AllowHoldsOnDamagedItems'));
1221
1222     my $on_shelf_holds = _OnShelfHoldsAllowed($itype,$borrower->{categorycode},$item->{holdingbranch});
1223
1224     if ( $on_shelf_holds == 1 ) {
1225         return 1;
1226     } elsif ( $on_shelf_holds == 2 ) {
1227         my @items =
1228           Koha::Items->search( { biblionumber => $item->{biblionumber} } );
1229
1230         my $any_available = 0;
1231
1232         foreach my $i (@items) {
1233             $any_available = 1
1234               unless $i->itemlost
1235               || $i->notforloan > 0
1236               || $i->withdrawn
1237               || $i->onloan
1238               || IsItemOnHoldAndFound( $i->id )
1239               || ( $i->damaged
1240                 && !C4::Context->preference('AllowHoldsOnDamagedItems') )
1241               || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan;
1242         }
1243
1244         return $any_available ? 0 : 1;
1245     }
1246
1247     return $item->{onloan} || GetReserveStatus($item->{itemnumber}) eq "Waiting";
1248 }
1249
1250 =head2 OnShelfHoldsAllowed
1251
1252   OnShelfHoldsAllowed($itemtype,$borrowercategory,$branchcode);
1253
1254 Checks issuingrules, using the borrowers categorycode, the itemtype, and branchcode to see if onshelf
1255 holds are allowed, returns true if so.
1256
1257 =cut
1258
1259 sub OnShelfHoldsAllowed {
1260     my ($item, $borrower) = @_;
1261
1262     my $itype = _get_itype($item);
1263     return _OnShelfHoldsAllowed($itype,$borrower->{categorycode},$item->{holdingbranch});
1264 }
1265
1266 sub _get_itype {
1267     my $item = shift;
1268
1269     my $itype;
1270     if (C4::Context->preference('item-level_itypes')) {
1271         # We can't trust GetItem to honour the syspref, so safest to do it ourselves
1272         # When GetItem is fixed, we can remove this
1273         $itype = $item->{itype};
1274     }
1275     else {
1276         # XXX This is a bit dodgy. It relies on biblio itemtype column having different name.
1277         # So if we already have a biblioitems join when calling this function,
1278         # we don't need to access the database again
1279         $itype = $item->{itemtype};
1280     }
1281     unless ($itype) {
1282         my $dbh = C4::Context->dbh;
1283         my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? ";
1284         my $sth = $dbh->prepare($query);
1285         $sth->execute($item->{biblioitemnumber});
1286         if (my $data = $sth->fetchrow_hashref()){
1287             $itype = $data->{itemtype};
1288         }
1289     }
1290     return $itype;
1291 }
1292
1293 sub _OnShelfHoldsAllowed {
1294     my ($itype,$borrowercategory,$branchcode) = @_;
1295
1296     my $issuing_rule = Koha::IssuingRules->get_effective_issuing_rule({ categorycode => $borrowercategory, itemtype => $itype, branchcode => $branchcode });
1297     return $issuing_rule ? $issuing_rule->onshelfholds : undef;
1298 }
1299
1300 =head2 AlterPriority
1301
1302   AlterPriority( $where, $reserve_id );
1303
1304 This function changes a reserve's priority up, down, to the top, or to the bottom.
1305 Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed
1306
1307 =cut
1308
1309 sub AlterPriority {
1310     my ( $where, $reserve_id ) = @_;
1311
1312     my $hold = Koha::Holds->find( $reserve_id );
1313     return unless $hold;
1314
1315     if ( $hold->cancellationdate ) {
1316         warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (" . $hold->cancellationdate . ')';
1317         return;
1318     }
1319
1320     if ( $where eq 'up' || $where eq 'down' ) {
1321
1322       my $priority = $hold->priority;
1323       $priority = $where eq 'up' ? $priority - 1 : $priority + 1;
1324       _FixPriority({ reserve_id => $reserve_id, rank => $priority })
1325
1326     } elsif ( $where eq 'top' ) {
1327
1328       _FixPriority({ reserve_id => $reserve_id, rank => '1' })
1329
1330     } elsif ( $where eq 'bottom' ) {
1331
1332       _FixPriority({ reserve_id => $reserve_id, rank => '999999' });
1333
1334     }
1335     # FIXME Should return the new priority
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 $hold = Koha::Holds->find( $reserve_id );
1472         $biblionumber = $hold->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;