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