X-Git-Url: http://git.rot13.org/?a=blobdiff_plain;f=C4%2FReserves.pm;h=974168f8a38cedbd870f90f368d00a4adf502041;hb=102b1ca4bf3b1721f496417473643ff951c13833;hp=529623a8e009747844853461478b63c2ae0a4b6f;hpb=f9b9c78231339a54f1ba61df1b0a0b239a4f91c7;p=koha.git diff --git a/C4/Reserves.pm b/C4/Reserves.pm index 529623a8e0..974168f8a3 100644 --- a/C4/Reserves.pm +++ b/C4/Reserves.pm @@ -34,18 +34,28 @@ use C4::Accounts; use C4::Members::Messaging; use C4::Members qw(); use C4::Letters; -use C4::Branch qw( GetBranchDetail ); +use C4::Log; +use Koha::Biblios; use Koha::DateUtils; use Koha::Calendar; use Koha::Database; use Koha::Hold; +use Koha::Old::Hold; use Koha::Holds; +use Koha::Libraries; +use Koha::IssuingRules; +use Koha::Items; +use Koha::ItemTypes; +use Koha::Patrons; +use Koha::CirculationRules; +use Koha::Account::Lines; use List::MoreUtils qw( firstidx any ); use Carp; +use Data::Dumper; -use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); +use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); =head1 NAME @@ -91,21 +101,11 @@ This modules provides somes functions to deal with reservations. =cut BEGIN { - # set the version for version checking - $VERSION = 3.07.00.049; require Exporter; @ISA = qw(Exporter); @EXPORT = qw( &AddReserve - &GetReserve - &GetReservesFromItemnumber - &GetReservesFromBiblionumber - &GetReservesFromBorrowernumber - &GetReservesForBranch - &GetReservesToBranch - &GetReserveCount - &GetReserveInfo &GetReserveStatus &GetOtherReserves @@ -122,15 +122,12 @@ BEGIN { &CanBookBeReserved &CanItemBeReserved &CanReserveBeCanceledFromOpac - &CancelReserve &CancelExpiredReserves &AutoUnsuspendReserves &IsAvailableForItemLevelRequest - &OPACItemHoldsAllowed - &AlterPriority &ToggleLowestPriority @@ -141,6 +138,8 @@ BEGIN { &GetReservesControlBranch IsItemOnHoldAndFound + + GetMaxPatronHoldsForRecord ); @EXPORT_OK = qw( MergeHolds ); } @@ -158,28 +157,32 @@ The following tables are available witin the HOLDPLACED message: biblio biblioitems items + reserves =cut sub AddReserve { my ( - $branch, $borrowernumber, $biblionumber, - $bibitems, $priority, $resdate, $expdate, $notes, - $title, $checkitem, $found + $branch, $borrowernumber, $biblionumber, $bibitems, + $priority, $resdate, $expdate, $notes, + $title, $checkitem, $found, $itemtype ) = @_; - if ( Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->count() > 0 ) { - carp("AddReserve: borrower $borrowernumber already has a hold for biblionumber $biblionumber"); - return; - } - - my $dbh = C4::Context->dbh; - $resdate = output_pref( { str => dt_from_string( $resdate ), dateonly => 1, dateformat => 'iso' }) or output_pref({ dt => dt_from_string, dateonly => 1, dateformat => 'iso' }); $expdate = output_pref({ str => $expdate, dateonly => 1, dateformat => 'iso' }); + # if we have an item selectionned, and the pickup branch is the same as the holdingbranch + # of the document, we force the value $priority and $found . + if ( $checkitem and not C4::Context->preference('ReservesNeedReturns') ) { + $priority = 0; + my $item = Koha::Items->find( $checkitem ); # FIXME Prevent bad calls + if ( $item->holdingbranch eq $branch ) { + $found = 'W'; + } + } + if ( C4::Context->preference('AllowHoldDateInFuture') ) { # Make room in reserves for this before those of a later reserve date @@ -193,6 +196,9 @@ sub AddReserve { $waitingdate = $resdate; } + # Don't add itemtype limit if specific item is selected + $itemtype = undef if $checkitem; + # updates take place here my $hold = Koha::Hold->new( { @@ -205,35 +211,45 @@ sub AddReserve { itemnumber => $checkitem, found => $found, waitingdate => $waitingdate, - expirationdate => $expdate + expirationdate => $expdate, + itemtype => $itemtype, } )->store(); + $hold->set_waiting() if $found eq 'W'; + + logaction( 'HOLDS', 'CREATE', $hold->id, Dumper($hold->unblessed) ) + if C4::Context->preference('HoldsLog'); + my $reserve_id = $hold->id(); # add a reserve fee if needed - my $fee = GetReserveFee( $borrowernumber, $biblionumber ); - ChargeReserveFee( $borrowernumber, $fee, $title ); + if ( C4::Context->preference('HoldFeeMode') ne 'any_time_is_collected' ) { + my $reserve_fee = GetReserveFee( $borrowernumber, $biblionumber ); + ChargeReserveFee( $borrowernumber, $reserve_fee, $title ); + } _FixPriority({ biblionumber => $biblionumber}); # Send e-mail to librarian if syspref is active if(C4::Context->preference("emailLibrarianWhenHoldIsPlaced")){ - my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber); - my $branch_details = C4::Branch::GetBranchDetail($borrower->{branchcode}); + my $patron = Koha::Patrons->find( $borrowernumber ); + my $library = $patron->library; if ( my $letter = C4::Letters::GetPreparedLetter ( module => 'reserves', letter_code => 'HOLDPLACED', branchcode => $branch, + lang => $patron->lang, tables => { - 'branches' => $branch_details, - 'borrowers' => $borrower, + 'branches' => $library->unblessed, + 'borrowers' => $patron->unblessed, 'biblio' => $biblionumber, 'biblioitems' => $biblionumber, 'items' => $checkitem, + 'reserves' => $hold->unblessed, }, ) ) { - my $admin_email_address =$branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress'); + my $admin_email_address = $library->branchemail || C4::Context->preference('KohaAdminEmailAddress'); C4::Letters::EnqueueLetter( { letter => $letter, @@ -249,166 +265,9 @@ sub AddReserve { return $reserve_id; } -=head2 GetReserve - - $res = GetReserve( $reserve_id ); - - Return the current reserve. - -=cut - -sub GetReserve { - my ($reserve_id) = @_; - - my $dbh = C4::Context->dbh; - my $query = "SELECT * FROM reserves WHERE reserve_id = ?"; - my $sth = $dbh->prepare( $query ); - $sth->execute( $reserve_id ); - return $sth->fetchrow_hashref(); -} - -=head2 GetReservesFromBiblionumber - - my $reserves = GetReservesFromBiblionumber({ - biblionumber => $biblionumber, - [ itemnumber => $itemnumber, ] - [ all_dates => 1|0 ] - }); - -This function gets the list of reservations for one C<$biblionumber>, -returning an arrayref pointing to the reserves for C<$biblionumber>. - -By default, only reserves whose start date falls before the current -time are returned. To return all reserves, including future ones, -the C parameter can be included and set to a true value. - -If the C parameter is supplied, reserves must be targeted -to that item or not targeted to any item at all; otherwise, they -are excluded from the list. - -=cut - -sub GetReservesFromBiblionumber { - my ( $params ) = @_; - my $biblionumber = $params->{biblionumber} or return []; - my $itemnumber = $params->{itemnumber}; - my $all_dates = $params->{all_dates} // 0; - my $dbh = C4::Context->dbh; - - # Find the desired items in the reserves - my @params; - my $query = " - SELECT reserve_id, - branchcode, - timestamp AS rtimestamp, - priority, - biblionumber, - borrowernumber, - reservedate, - found, - itemnumber, - reservenotes, - expirationdate, - lowestPriority, - suspend, - suspend_until - FROM reserves - WHERE biblionumber = ? "; - push( @params, $biblionumber ); - unless ( $all_dates ) { - $query .= " AND reservedate <= CAST(NOW() AS DATE) "; - } - if ( $itemnumber ) { - $query .= " AND ( itemnumber IS NULL OR itemnumber = ? )"; - push( @params, $itemnumber ); - } - $query .= "ORDER BY priority"; - my $sth = $dbh->prepare($query); - $sth->execute( @params ); - my @results; - while ( my $data = $sth->fetchrow_hashref ) { - push @results, $data; - } - return \@results; -} - -=head2 GetReservesFromItemnumber - - ( $reservedate, $borrowernumber, $branchcode, $reserve_id, $waitingdate ) = GetReservesFromItemnumber($itemnumber); - -Get the first reserve for a specific item number (based on priority). Returns the abovementioned values for that reserve. - -The routine does not look at future reserves (read: item level holds), but DOES include future waits (a confirmed future hold). - -=cut - -sub GetReservesFromItemnumber { - my ($itemnumber) = @_; - - my $schema = Koha::Database->new()->schema(); - - my $r = $schema->resultset('Reserve')->search( - { - itemnumber => $itemnumber, - suspend => 0, - -or => [ - reservedate => \'<= CAST( NOW() AS DATE )', - waitingdate => { '!=', undef } - ] - }, - { - order_by => 'priority', - } - )->first(); - - return unless $r; - - return ( - $r->reservedate(), - $r->get_column('borrowernumber'), - $r->get_column('branchcode'), - $r->reserve_id(), - $r->waitingdate(), - ); -} - -=head2 GetReservesFromBorrowernumber - - $borrowerreserv = GetReservesFromBorrowernumber($borrowernumber,$tatus); - -TODO :: Descritpion - -=cut - -sub GetReservesFromBorrowernumber { - my ( $borrowernumber, $status ) = @_; - my $dbh = C4::Context->dbh; - my $sth; - if ($status) { - $sth = $dbh->prepare(" - SELECT * - FROM reserves - WHERE borrowernumber=? - AND found =? - ORDER BY reservedate - "); - $sth->execute($borrowernumber,$status); - } else { - $sth = $dbh->prepare(" - SELECT * - FROM reserves - WHERE borrowernumber=? - ORDER BY reservedate - "); - $sth->execute($borrowernumber); - } - my $data = $sth->fetchall_arrayref({}); - return @$data; -} - =head2 CanBookBeReserved - $canReserve = &CanBookBeReserved($borrowernumber, $biblionumber) + $canReserve = &CanBookBeReserved($borrowernumber, $biblionumber, $branchcode) if ($canReserve eq 'OK') { #We can reserve this Item! } See CanItemBeReserved() for possible return values. @@ -416,143 +275,190 @@ See CanItemBeReserved() for possible return values. =cut sub CanBookBeReserved{ - my ($borrowernumber, $biblionumber) = @_; + my ($borrowernumber, $biblionumber, $pickup_branchcode) = @_; - my $items = GetItemnumbersForBiblio($biblionumber); + my @itemnumbers = Koha::Items->search({ biblionumber => $biblionumber})->get_column("itemnumber"); #get items linked via host records my @hostitems = get_hostitemnumbers_of($biblionumber); if (@hostitems){ - push (@$items,@hostitems); + push (@itemnumbers, @hostitems); } - my $canReserve; - foreach my $item (@$items) { - $canReserve = CanItemBeReserved( $borrowernumber, $item ); - return 'OK' if $canReserve eq 'OK'; + my $canReserve = { status => '' }; + foreach my $itemnumber (@itemnumbers) { + $canReserve = CanItemBeReserved( $borrowernumber, $itemnumber, $pickup_branchcode ); + return { status => 'OK' } if $canReserve->{status} eq 'OK'; } return $canReserve; } =head2 CanItemBeReserved - $canReserve = &CanItemBeReserved($borrowernumber, $itemnumber) - if ($canReserve eq 'OK') { #We can reserve this Item! } + $canReserve = &CanItemBeReserved($borrowernumber, $itemnumber, $branchcode) + if ($canReserve->{status} eq 'OK') { #We can reserve this Item! } -@RETURNS OK, if the Item can be reserved. - ageRestricted, if the Item is age restricted for this borrower. - damaged, if the Item is damaged. - cannotReserveFromOtherBranches, if syspref 'canreservefromotherbranches' is OK. - tooManyReserves, if the borrower has exceeded his maximum reserve amount. - notReservable, if holds on this item are not allowed +@RETURNS { status => OK }, if the Item can be reserved. + { status => ageRestricted }, if the Item is age restricted for this borrower. + { status => damaged }, if the Item is damaged. + { status => cannotReserveFromOtherBranches }, if syspref 'canreservefromotherbranches' is OK. + { status => tooManyReserves, limit => $limit }, if the borrower has exceeded their maximum reserve amount. + { status => notReservable }, if holds on this item are not allowed + { status => libraryNotFound }, if given branchcode is not an existing library + { status => libraryNotPickupLocation }, if given branchcode is not configured to be a pickup location + { status => cannotBeTransferred }, if branch transfer limit applies on given item and branchcode =cut -sub CanItemBeReserved{ - my ($borrowernumber, $itemnumber) = @_; +sub CanItemBeReserved { + my ( $borrowernumber, $itemnumber, $pickup_branchcode ) = @_; + + my $dbh = C4::Context->dbh; + my $ruleitemtype; # itemtype of the matching issuing rule + my $allowedreserves = 0; # Total number of holds allowed across all records + my $holds_per_record = 1; # Total number of holds allowed for this one given record + my $holds_per_day; # Default to unlimited - my $dbh = C4::Context->dbh; - my $ruleitemtype; # itemtype of the matching issuing rule - my $allowedreserves = 0; - # we retrieve borrowers and items informations # # item->{itype} will come for biblioitems if necessery - my $item = GetItem($itemnumber); - my $biblioData = C4::Biblio::GetBiblioData( $item->{biblionumber} ); - my $borrower = C4::Members::GetMember('borrowernumber'=>$borrowernumber); + my $item = C4::Items::GetItem($itemnumber); + my $biblio = Koha::Biblios->find( $item->{biblionumber} ); + my $patron = Koha::Patrons->find( $borrowernumber ); + my $borrower = $patron->unblessed; # If an item is damaged and we don't allow holds on damaged items, we can stop right here - return 'damaged' if ( $item->{damaged} && !C4::Context->preference('AllowHoldsOnDamagedItems') ); + return { status =>'damaged' } + if ( $item->{damaged} + && !C4::Context->preference('AllowHoldsOnDamagedItems') ); - #Check for the age restriction - my ($ageRestriction, $daysToAgeRestriction) = C4::Circulation::GetAgeRestriction( $biblioData->{agerestriction}, $borrower ); - return 'ageRestricted' if $daysToAgeRestriction && $daysToAgeRestriction > 0; + # Check for the age restriction + my ( $ageRestriction, $daysToAgeRestriction ) = + C4::Circulation::GetAgeRestriction( $biblio->biblioitem->agerestriction, $borrower ); + return { status => 'ageRestricted' } if $daysToAgeRestriction && $daysToAgeRestriction > 0; + + # Check that the patron doesn't have an item level hold on this item already + return { status =>'itemAlreadyOnHold' } + if Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->count(); my $controlbranch = C4::Context->preference('ReservesControlBranch'); - # we retrieve user rights on this itemtype and branchcode - my $sth = $dbh->prepare("SELECT categorycode, itemtype, branchcode, reservesallowed - FROM issuingrules - WHERE (categorycode in (?,'*') ) - AND (itemtype IN (?,'*')) - AND (branchcode IN (?,'*')) - ORDER BY - categorycode DESC, - itemtype DESC, - branchcode DESC;" - ); - - my $querycount ="SELECT - count(*) as count - FROM reserves - LEFT JOIN items USING (itemnumber) - LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber) - LEFT JOIN borrowers USING (borrowernumber) - WHERE borrowernumber = ? - "; - - - my $branchcode = ""; - my $branchfield = "reserves.branchcode"; - - if( $controlbranch eq "ItemHomeLibrary" ){ + my $querycount = q{ + SELECT count(*) AS count + FROM reserves + LEFT JOIN items USING (itemnumber) + LEFT JOIN biblioitems ON (reserves.biblionumber=biblioitems.biblionumber) + LEFT JOIN borrowers USING (borrowernumber) + WHERE borrowernumber = ? + }; + + my $branchcode = ""; + my $branchfield = "reserves.branchcode"; + + if ( $controlbranch eq "ItemHomeLibrary" ) { $branchfield = "items.homebranch"; - $branchcode = $item->{homebranch}; - }elsif( $controlbranch eq "PatronLibrary" ){ + $branchcode = $item->{homebranch}; + } + elsif ( $controlbranch eq "PatronLibrary" ) { $branchfield = "borrowers.branchcode"; - $branchcode = $borrower->{branchcode}; - } - - # we retrieve rights - $sth->execute($borrower->{'categorycode'}, $item->{'itype'}, $branchcode); - if(my $rights = $sth->fetchrow_hashref()){ - $ruleitemtype = $rights->{itemtype}; - $allowedreserves = $rights->{reservesallowed}; - }else{ + $branchcode = $borrower->{branchcode}; + } + + # we retrieve rights + if ( my $rights = GetHoldRule( $borrower->{'categorycode'}, $item->{'itype'}, $branchcode ) ) { + $ruleitemtype = $rights->{itemtype}; + $allowedreserves = $rights->{reservesallowed}; + $holds_per_record = $rights->{holds_per_record}; + $holds_per_day = $rights->{holds_per_day}; + } + else { $ruleitemtype = '*'; } + $item = Koha::Items->find( $itemnumber ); + my $holds = Koha::Holds->search( + { + borrowernumber => $borrowernumber, + biblionumber => $item->biblionumber, + found => undef, # Found holds don't count against a patron's holds limit + } + ); + if ( $holds->count() >= $holds_per_record ) { + return { status => "tooManyHoldsForThisRecord", limit => $holds_per_record }; + } + + my $today_holds = Koha::Holds->search({ + borrowernumber => $borrowernumber, + reservedate => dt_from_string->date + }); + + if ( defined $holds_per_day && + ( ( $holds_per_day > 0 && $today_holds->count() >= $holds_per_day ) + or ( $holds_per_day == 0 ) ) + ) { + return { status => 'tooManyReservesToday', limit => $holds_per_day }; + } + # we retrieve count $querycount .= "AND $branchfield = ?"; - + # If using item-level itypes, fall back to the record # level itemtype if the hold has no associated item $querycount .= C4::Context->preference('item-level_itypes') - ? " AND COALESCE( itype, itemtype ) = ?" - : " AND itemtype = ?" + ? " AND COALESCE( items.itype, biblioitems.itemtype ) = ?" + : " AND biblioitems.itemtype = ?" if ( $ruleitemtype ne "*" ); my $sthcount = $dbh->prepare($querycount); - - if($ruleitemtype eq "*"){ - $sthcount->execute($borrowernumber, $branchcode); - }else{ - $sthcount->execute($borrowernumber, $branchcode, $ruleitemtype); + + if ( $ruleitemtype eq "*" ) { + $sthcount->execute( $borrowernumber, $branchcode ); + } + else { + $sthcount->execute( $borrowernumber, $branchcode, $ruleitemtype ); } my $reservecount = "0"; - if(my $rowcount = $sthcount->fetchrow_hashref()){ + if ( my $rowcount = $sthcount->fetchrow_hashref() ) { $reservecount = $rowcount->{count}; } + # we check if it's ok or not - if( $reservecount >= $allowedreserves ){ - return 'tooManyReserves'; + if ( $reservecount >= $allowedreserves ) { + return { status => 'tooManyReserves', limit => $allowedreserves }; + } + + # Now we need to check hold limits by patron category + my $rule = Koha::CirculationRules->get_effective_rule( + { + categorycode => $borrower->{categorycode}, + branchcode => $branchcode, + rule_name => 'max_holds', + } + ); + if ( $rule && defined( $rule->rule_value ) && $rule->rule_value ne '' ) { + my $total_holds_count = Koha::Holds->search( + { + borrowernumber => $borrower->{borrowernumber} + } + )->count(); + + return { status => 'tooManyReserves', limit => $rule->rule_value} if $total_holds_count >= $rule->rule_value; } - my $circ_control_branch = C4::Circulation::_GetCircControlBranch($item, - $borrower); - my $branchitemrule = C4::Circulation::GetBranchItemRule($circ_control_branch, - $item->{itype}); + my $circ_control_branch = + C4::Circulation::_GetCircControlBranch( $item->unblessed(), $borrower ); + my $branchitemrule = + C4::Circulation::GetBranchItemRule( $circ_control_branch, $item->itype ); if ( $branchitemrule->{holdallowed} == 0 ) { - return 'notReservable'; + return { status => 'notReservable' }; } if ( $branchitemrule->{holdallowed} == 1 - && $borrower->{branchcode} ne $item->{homebranch} ) + && $borrower->{branchcode} ne $item->homebranch ) { - return 'cannotReserveFromOtherBranches'; + return { status => 'cannotReserveFromOtherBranches' }; } # If reservecount is ok, we check item branch if IndependentBranches is ON @@ -560,13 +466,29 @@ sub CanItemBeReserved{ if ( C4::Context->preference('IndependentBranches') and !C4::Context->preference('canreservefromotherbranches') ) { - my $itembranch = $item->{homebranch}; - if ($itembranch ne $borrower->{branchcode}) { - return 'cannotReserveFromOtherBranches'; + my $itembranch = $item->homebranch; + if ( $itembranch ne $borrower->{branchcode} ) { + return { status => 'cannotReserveFromOtherBranches' }; + } + } + + if ($pickup_branchcode) { + my $destination = Koha::Libraries->find({ + branchcode => $pickup_branchcode, + }); + + unless ($destination) { + return { status => 'libraryNotFound' }; + } + unless ($destination->pickup_location) { + return { status => 'libraryNotPickupLocation' }; + } + unless ($item->can_be_transferred({ to => $destination })) { + return 'cannotBeTransferred'; } } - return 'OK'; + return { status => 'OK' }; } =head2 CanReserveBeCanceledFromOpac @@ -583,39 +505,15 @@ sub CanReserveBeCanceledFromOpac { my ($reserve_id, $borrowernumber) = @_; return unless $reserve_id and $borrowernumber; - my $reserve = GetReserve($reserve_id); + my $reserve = Koha::Holds->find($reserve_id); - return 0 unless $reserve->{borrowernumber} == $borrowernumber; - return 0 if ( $reserve->{found} eq 'W' ) or ( $reserve->{found} eq 'T' ); + return 0 unless $reserve->borrowernumber == $borrowernumber; + return 0 if ( $reserve->found eq 'W' ) or ( $reserve->found eq 'T' ); return 1; } -=head2 GetReserveCount - - $number = &GetReserveCount($borrowernumber); - -this function returns the number of reservation for a borrower given on input arg. - -=cut - -sub GetReserveCount { - my ($borrowernumber) = @_; - - my $dbh = C4::Context->dbh; - - my $query = " - SELECT COUNT(*) AS counter - FROM reserves - WHERE borrowernumber = ? - "; - my $sth = $dbh->prepare($query); - $sth->execute($borrowernumber); - my $row = $sth->fetchrow_hashref; - return $row->{counter}; -} - =head2 GetOtherReserves ($messages,$nextreservinfo)=$GetOtherReserves(itemnumber); @@ -674,13 +572,24 @@ sub GetOtherReserves { sub ChargeReserveFee { my ( $borrowernumber, $fee, $title ) = @_; - return if !$fee || $fee==0; # the last test is needed to include 0.00 - my $accquery = qq{ -INSERT INTO accountlines ( borrowernumber, accountno, date, amount, description, accounttype, amountoutstanding ) VALUES (?, ?, NOW(), ?, ?, 'Res', ?) - }; - my $dbh = C4::Context->dbh; - my $nextacctno = &getnextacctno( $borrowernumber ); - $dbh->do( $accquery, undef, ( $borrowernumber, $nextacctno, $fee, "Reserve Charge - $title", $fee ) ); + + return if !$fee || $fee == 0; # the last test is needed to include 0.00 + + my $branchcode = C4::Context->userenv ? C4::Context->userenv->{'branch'} : undef; + my $nextacctno = C4::Accounts::getnextacctno($borrowernumber); + + Koha::Account::Line->new( + { + borrowernumber => $borrowernumber, + accountno => $nextacctno, + date => dt_from_string(), + amount => $fee, + description => "Reserve Charge - $title", + accounttype => 'Res', + amountoutstanding => $fee, + branchcode => $branchcode + } + )->store(); } =head2 GetReserveFee @@ -707,7 +616,8 @@ SELECT COUNT(*) FROM reserves WHERE biblionumber=? AND borrowernumber<>? my $dbh = C4::Context->dbh; my ( $fee ) = $dbh->selectrow_array( $borquery, undef, ($borrowernumber) ); - if( $fee && $fee > 0 ) { + my $hold_fee_mode = C4::Context->preference('HoldFeeMode') || 'not_always'; + if( $fee and $fee > 0 and $hold_fee_mode eq 'not_always' ) { # This is a reconstruction of the old code: # Compare number of items with items issued, and optionally check holds # If not all items are issued and there are no holds: charge no fee @@ -724,68 +634,6 @@ SELECT COUNT(*) FROM reserves WHERE biblionumber=? AND borrowernumber<>? return $fee; } -=head2 GetReservesToBranch - - @transreserv = GetReservesToBranch( $frombranch ); - -Get reserve list for a given branch - -=cut - -sub GetReservesToBranch { - my ( $frombranch ) = @_; - my $dbh = C4::Context->dbh; - my $sth = $dbh->prepare( - "SELECT reserve_id,borrowernumber,reservedate,itemnumber,timestamp - FROM reserves - WHERE priority='0' - AND branchcode=?" - ); - $sth->execute( $frombranch ); - my @transreserv; - my $i = 0; - while ( my $data = $sth->fetchrow_hashref ) { - $transreserv[$i] = $data; - $i++; - } - return (@transreserv); -} - -=head2 GetReservesForBranch - - @transreserv = GetReservesForBranch($frombranch); - -=cut - -sub GetReservesForBranch { - my ($frombranch) = @_; - my $dbh = C4::Context->dbh; - - my $query = " - SELECT reserve_id,borrowernumber,reservedate,itemnumber,waitingdate - FROM reserves - WHERE priority='0' - AND found='W' - "; - $query .= " AND branchcode=? " if ( $frombranch ); - $query .= "ORDER BY waitingdate" ; - - my $sth = $dbh->prepare($query); - if ($frombranch){ - $sth->execute($frombranch); - } else { - $sth->execute(); - } - - my @transreserv; - my $i = 0; - while ( my $data = $sth->fetchrow_hashref ) { - $transreserv[$i] = $data; - $i++; - } - return (@transreserv); -} - =head2 GetReserveStatus $reservestatus = GetReserveStatus($itemnumber); @@ -921,14 +769,18 @@ sub CheckReserves { my $priority = 10000000; foreach my $res (@reserves) { if ( $res->{'itemnumber'} == $itemnumber && $res->{'priority'} == 0) { - return ( "Waiting", $res, \@reserves ); # Found it + if ($res->{'found'} eq 'W') { + return ( "Waiting", $res, \@reserves ); # Found it, it is waiting + } else { + return ( "Reserved", $res, \@reserves ); # Found determinated hold, e. g. the tranferred one + } } else { - my $borrowerinfo; + my $patron; my $iteminfo; my $local_hold_match; if ($LocalHoldsPriority) { - $borrowerinfo = C4::Members::GetMember( borrowernumber => $res->{'borrowernumber'} ); + $patron = Koha::Patrons->find( $res->{borrowernumber} ); $iteminfo = C4::Items::GetItem($itemnumber); my $local_holds_priority_item_branchcode = @@ -937,7 +789,7 @@ sub CheckReserves { ( $LocalHoldsPriorityPatronControl eq 'PickupLibrary' ) ? $res->{branchcode} : ( $LocalHoldsPriorityPatronControl eq 'HomeLibrary' ) - ? $borrowerinfo->{branchcode} + ? $patron->branchcode : undef; $local_hold_match = $local_holds_priority_item_branchcode eq @@ -946,12 +798,14 @@ sub CheckReserves { # See if this item is more important than what we've got so far if ( ( $res->{'priority'} && $res->{'priority'} < $priority ) || $local_hold_match ) { - $borrowerinfo ||= C4::Members::GetMember( borrowernumber => $res->{'borrowernumber'} ); $iteminfo ||= C4::Items::GetItem($itemnumber); - my $branch = GetReservesControlBranch( $iteminfo, $borrowerinfo ); + next if $res->{itemtype} && $res->{itemtype} ne _get_itype( $iteminfo ); + $patron ||= Koha::Patrons->find( $res->{borrowernumber} ); + my $branch = GetReservesControlBranch( $iteminfo, $patron->unblessed ); my $branchitemrule = C4::Circulation::GetBranchItemRule($branch,$iteminfo->{'itype'}); next if ($branchitemrule->{'holdallowed'} == 0); - next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $borrowerinfo->{'branchcode'})); + next if (($branchitemrule->{'holdallowed'} == 1) && ($branch ne $patron->branchcode)); + next if ( ($branchitemrule->{hold_fulfillment_policy} ne 'any') && ($res->{branchcode} ne $iteminfo->{ $branchitemrule->{hold_fulfillment_policy} }) ); $priority = $res->{'priority'}; $highest = $res; last if $local_hold_match; @@ -979,48 +833,28 @@ Cancels all reserves with an expiration date from before today. =cut sub CancelExpiredReserves { + my $today = dt_from_string(); + my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays'); + my $expireWaiting = C4::Context->preference('ExpireReservesMaxPickUpDelay'); - # Cancel reserves that have passed their expiration date. - my $dbh = C4::Context->dbh; - my $sth = $dbh->prepare( " - SELECT * FROM reserves WHERE DATE(expirationdate) < DATE( CURDATE() ) - AND expirationdate IS NOT NULL - AND found IS NULL - " ); - $sth->execute(); - - while ( my $res = $sth->fetchrow_hashref() ) { - CancelReserve({ reserve_id => $res->{'reserve_id'} }); - } - - # Cancel reserves that have been waiting too long - if ( C4::Context->preference("ExpireReservesMaxPickUpDelay") ) { - my $max_pickup_delay = C4::Context->preference("ReservesMaxPickUpDelay"); - my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays'); + my $dtf = Koha::Database->new->schema->storage->datetime_parser; + my $params = { expirationdate => { '<', $dtf->format_date($today) } }; + $params->{found} = undef unless $expireWaiting; - my $today = dt_from_string(); + # FIXME To move to Koha::Holds->search_expired (?) + my $holds = Koha::Holds->search( $params ); - my $query = "SELECT * FROM reserves WHERE TO_DAYS( NOW() ) - TO_DAYS( waitingdate ) > ? AND found = 'W' AND priority = 0"; - $sth = $dbh->prepare( $query ); - $sth->execute( $max_pickup_delay ); + while ( my $hold = $holds->next ) { + my $calendar = Koha::Calendar->new( branchcode => $hold->branchcode ); - while ( my $res = $sth->fetchrow_hashref ) { - my $do_cancel = 1; - unless ( $cancel_on_holidays ) { - my $calendar = Koha::Calendar->new( branchcode => $res->{'branchcode'} ); - my $is_holiday = $calendar->is_holiday( $today ); + next if !$cancel_on_holidays && $calendar->is_holiday( $today ); - if ( $is_holiday ) { - $do_cancel = 0; - } - } - - if ( $do_cancel ) { - CancelReserve({ reserve_id => $res->{'reserve_id'}, charge_cancel_fee => 1 }); - } + my $cancel_params = {}; + if ( $hold->found eq 'W' ) { + $cancel_params->{charge_cancel_fee} = 1; } + $hold->cancel( $cancel_params ); } - } =head2 AutoUnsuspendReserves @@ -1032,73 +866,11 @@ Unsuspends all suspended reserves with a suspend_until date from before today. =cut sub AutoUnsuspendReserves { + my $today = dt_from_string(); - my $dbh = C4::Context->dbh; - - my $query = "UPDATE reserves SET suspend = 0, suspend_until = NULL WHERE DATE( suspend_until ) < DATE( CURDATE() )"; - my $sth = $dbh->prepare( $query ); - $sth->execute(); - -} - -=head2 CancelReserve - - CancelReserve({ reserve_id => $reserve_id, [ biblionumber => $biblionumber, borrowernumber => $borrrowernumber, itemnumber => $itemnumber, ] [ charge_cancel_fee => 1 ] }); - -Cancels a reserve. If C is passed and the C syspref is set, charge that fee to the patron's account. - -=cut - -sub CancelReserve { - my ( $params ) = @_; - - my $reserve_id = $params->{'reserve_id'}; - # Filter out only the desired keys; this will insert undefined values for elements missing in - # \%params, but GetReserveId filters them out anyway. - $reserve_id = GetReserveId( { biblionumber => $params->{'biblionumber'}, borrowernumber => $params->{'borrowernumber'}, itemnumber => $params->{'itemnumber'} } ) unless ( $reserve_id ); - - return unless ( $reserve_id ); + my @holds = Koha::Holds->search( { suspend_until => { '<=' => $today->ymd() } } ); - my $dbh = C4::Context->dbh; - - my $reserve = GetReserve( $reserve_id ); - if ($reserve) { - my $query = " - UPDATE reserves - SET cancellationdate = now(), - found = Null, - priority = 0 - WHERE reserve_id = ? - "; - my $sth = $dbh->prepare($query); - $sth->execute( $reserve_id ); - - $query = " - INSERT INTO old_reserves - SELECT * FROM reserves - WHERE reserve_id = ? - "; - $sth = $dbh->prepare($query); - $sth->execute( $reserve_id ); - - $query = " - DELETE FROM reserves - WHERE reserve_id = ? - "; - $sth = $dbh->prepare($query); - $sth->execute( $reserve_id ); - - # now fix the priority on the others.... - _FixPriority({ biblionumber => $reserve->{biblionumber} }); - - # and, if desired, charge a cancel fee - my $charge = C4::Context->preference("ExpireReservesMaxPickUpDelayCharge"); - if ( $charge && $params->{'charge_cancel_fee'} ) { - manualinvoice($reserve->{'borrowernumber'}, $reserve->{'itemnumber'}, 'Hold waiting too long', 'F', $charge); - } - } - - return $reserve; + map { $_->suspend(0)->suspend_until(undef)->store() } @holds; } =head2 ModReserve @@ -1152,26 +924,42 @@ sub ModReserve { return if $rank eq "n"; return unless ( $reserve_id || ( $borrowernumber && ( $biblionumber || $itemnumber ) ) ); - $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber }) unless ( $reserve_id ); - my $dbh = C4::Context->dbh; + my $hold; + unless ( $reserve_id ) { + my $holds = Koha::Holds->search({ biblionumber => $biblionumber, borrowernumber => $borrowernumber, itemnumber => $itemnumber }); + return unless $holds->count; # FIXME Should raise an exception + $hold = $holds->next; + $reserve_id = $hold->reserve_id; + } + + $hold ||= Koha::Holds->find($reserve_id); + if ( $rank eq "del" ) { - CancelReserve({ reserve_id => $reserve_id }); + $hold->cancel; } elsif ($rank =~ /^\d+/ and $rank > 0) { - my $query = " - UPDATE reserves SET priority = ? ,branchcode = ?, itemnumber = ?, found = NULL, waitingdate = NULL - WHERE reserve_id = ? - "; - my $sth = $dbh->prepare($query); - $sth->execute( $rank, $branchcode, $itemnumber, $reserve_id ); + logaction( 'HOLDS', 'MODIFY', $hold->reserve_id, Dumper($hold->unblessed) ) + if C4::Context->preference('HoldsLog'); + + $hold->set( + { + priority => $rank, + branchcode => $branchcode, + itemnumber => $itemnumber, + found => undef, + waitingdate => undef + } + )->store(); if ( defined( $suspend_until ) ) { if ( $suspend_until ) { - $suspend_until = eval { output_pref( { dt => dt_from_string( $suspend_until ), dateonly => 1, dateformat => 'iso' }); }; - $dbh->do("UPDATE reserves SET suspend = 1, suspend_until = ? WHERE reserve_id = ?", undef, ( $suspend_until, $reserve_id ) ); + $suspend_until = eval { dt_from_string( $suspend_until ) }; + $hold->suspend_hold( $suspend_until ); } else { - $dbh->do("UPDATE reserves SET suspend_until = NULL WHERE reserve_id = ?", undef, ( $reserve_id ) ); + # If the hold is suspended leave the hold suspended, but convert it to an indefinite hold. + # If the hold is not suspended, this does nothing. + $hold->set( { suspend_until => undef } )->store(); } } @@ -1193,56 +981,35 @@ whose keys are fields from the reserves table in the Koha database. sub ModReserveFill { my ($res) = @_; - my $dbh = C4::Context->dbh; - # fill in a reserve record.... my $reserve_id = $res->{'reserve_id'}; - my $biblionumber = $res->{'biblionumber'}; - my $borrowernumber = $res->{'borrowernumber'}; - my $resdate = $res->{'reservedate'}; + + my $hold = Koha::Holds->find($reserve_id); # get the priority on this record.... - my $priority; - my $query = "SELECT priority - FROM reserves - WHERE biblionumber = ? - AND borrowernumber = ? - AND reservedate = ?"; - my $sth = $dbh->prepare($query); - $sth->execute( $biblionumber, $borrowernumber, $resdate ); - ($priority) = $sth->fetchrow_array; + my $priority = $hold->priority; - # update the database... - $query = "UPDATE reserves - SET found = 'F', - priority = 0 - WHERE biblionumber = ? - AND reservedate = ? - AND borrowernumber = ? - "; - $sth = $dbh->prepare($query); - $sth->execute( $biblionumber, $resdate, $borrowernumber ); - - # move to old_reserves - $query = "INSERT INTO old_reserves - SELECT * FROM reserves - WHERE biblionumber = ? - AND reservedate = ? - AND borrowernumber = ? - "; - $sth = $dbh->prepare($query); - $sth->execute( $biblionumber, $resdate, $borrowernumber ); - $query = "DELETE FROM reserves - WHERE biblionumber = ? - AND reservedate = ? - AND borrowernumber = ? - "; - $sth = $dbh->prepare($query); - $sth->execute( $biblionumber, $resdate, $borrowernumber ); + # update the hold statuses, no need to store it though, we will be deleting it anyway + $hold->set( + { + found => 'F', + priority => 0, + } + ); + + # FIXME Must call Koha::Hold->cancel ? => No, should call ->filled and add the correct log + Koha::Old::Hold->new( $hold->unblessed() )->store(); + + $hold->delete(); + + if ( C4::Context->preference('HoldFeeMode') eq 'any_time_is_collected' ) { + my $reserve_fee = GetReserveFee( $hold->borrowernumber, $hold->biblionumber ); + ChargeReserveFee( $hold->borrowernumber, $reserve_fee, $hold->biblio->title ); + } # now fix the priority on the others (if the priority wasn't # already sorted!).... unless ( $priority == 0 ) { - _FixPriority({ reserve_id => $reserve_id, biblionumber => $biblionumber }); + _FixPriority( { reserve_id => $reserve_id, biblionumber => $hold->biblionumber } ); } } @@ -1275,12 +1042,12 @@ sub ModReserveStatus { =head2 ModReserveAffect - &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend); + &ModReserveAffect($itemnumber,$borrowernumber,$diffBranchSend,$reserve_id); -This function affect an item and a status for a given reserve -The itemnumber parameter is used to find the biblionumber. -with the biblionumber & the borrowernumber, we can affect the itemnumber -to the correct reserve. +This function affect an item and a status for a given reserve, either fetched directly +by record_id, or by borrowernumber and itemnumber or biblionumber. If only biblionumber +is given, only first reserve returned is affected, which is ok for anything but +multi-item holds. if $transferToDo is not set, then the status is set to "Waiting" as well. otherwise, a transfer is on the way, and the end of the transfer will @@ -1289,7 +1056,7 @@ take care of the waiting status =cut sub ModReserveAffect { - my ( $itemnumber, $borrowernumber,$transferToDo ) = @_; + my ( $itemnumber, $borrowernumber, $transferToDo, $reserve_id ) = @_; my $dbh = C4::Context->dbh; # we want to attach $itemnumber to $borrowernumber, find the biblionumber @@ -1300,44 +1067,29 @@ sub ModReserveAffect { # get request - need to find out if item is already # waiting in order to not send duplicate hold filled notifications - my $reserve_id = GetReserveId({ - borrowernumber => $borrowernumber, - biblionumber => $biblionumber, - }); - return unless defined $reserve_id; - my $request = GetReserveInfo($reserve_id); - my $already_on_shelf = ($request && $request->{found} eq 'W') ? 1 : 0; - # If we affect a reserve that has to be transferred, don't set to Waiting - my $query; - if ($transferToDo) { - $query = " - UPDATE reserves - SET priority = 0, - itemnumber = ?, - found = 'T' - WHERE borrowernumber = ? - AND biblionumber = ? - "; - } - else { - # affect the reserve to Waiting as well. - $query = " - UPDATE reserves - SET priority = 0, - found = 'W', - waitingdate = NOW(), - itemnumber = ? - WHERE borrowernumber = ? - AND biblionumber = ? - "; - } - $sth = $dbh->prepare($query); - $sth->execute( $itemnumber, $borrowernumber,$biblionumber); - _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber ) if ( !$transferToDo && !$already_on_shelf ); + my $hold; + # Find hold by id if we have it + $hold = Koha::Holds->find( $reserve_id ) if $reserve_id; + # Find item level hold for this item if there is one + $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, itemnumber => $itemnumber } )->next(); + # Find record level hold if there is no item level hold + $hold ||= Koha::Holds->search( { borrowernumber => $borrowernumber, biblionumber => $biblionumber } )->next(); + + return unless $hold; + + my $already_on_shelf = $hold->found && $hold->found eq 'W'; + + $hold->itemnumber($itemnumber); + $hold->set_waiting($transferToDo); + + _koha_notify_reserve( $hold->reserve_id ) + if ( !$transferToDo && !$already_on_shelf ); + _FixPriority( { biblionumber => $biblionumber } ); + if ( C4::Context->preference("ReturnToShelvingCart") ) { - CartToShelf( $itemnumber ); + CartToShelf($itemnumber); } return; @@ -1357,7 +1109,9 @@ sub ModReserveCancelAll { my ( $itemnumber, $borrowernumber ) = @_; #step 1 : cancel the reservation - my $CancelReserve = CancelReserve({ itemnumber => $itemnumber, borrowernumber => $borrowernumber }); + my $holds = Koha::Holds->search({ itemnumber => $itemnumber, borrowernumber => $borrowernumber }); + return unless $holds->count; + $holds->next->cancel; #step 2 launch the subroutine of the others reserves ( $messages, $nextreservinfo ) = GetOtherReserves($itemnumber); @@ -1380,7 +1134,7 @@ sub ModReserveMinusPriority { my $dbh = C4::Context->dbh; my $query = " UPDATE reserves - SET priority = 0 , itemnumber = ? + SET priority = 0 , itemnumber = ? WHERE reserve_id = ? "; my $sth_upd = $dbh->prepare($query); @@ -1389,59 +1143,6 @@ sub ModReserveMinusPriority { _FixPriority({ reserve_id => $reserve_id, rank => '0' }); } -=head2 GetReserveInfo - - &GetReserveInfo($reserve_id); - -Get item and borrower details for a current hold. -Current implementation this query should have a single result. - -=cut - -sub GetReserveInfo { - my ( $reserve_id ) = @_; - my $dbh = C4::Context->dbh; - my $strsth="SELECT - reserve_id, - reservedate, - reservenotes, - reserves.borrowernumber, - reserves.biblionumber, - reserves.branchcode, - reserves.waitingdate, - notificationdate, - reminderdate, - priority, - found, - firstname, - surname, - phone, - email, - address, - address2, - cardnumber, - city, - zipcode, - biblio.title, - biblio.author, - items.holdingbranch, - items.itemcallnumber, - items.itemnumber, - items.location, - barcode, - notes - FROM reserves - LEFT JOIN items USING(itemnumber) - LEFT JOIN borrowers USING(borrowernumber) - LEFT JOIN biblio ON (reserves.biblionumber=biblio.biblionumber) - WHERE reserves.reserve_id = ?"; - my $sth = $dbh->prepare($strsth); - $sth->execute($reserve_id); - - my $data = $sth->fetchrow_hashref; - return $data; -} - =head2 IsAvailableForItemLevelRequest my $is_available = IsAvailableForItemLevelRequest($item_record,$borrower_record); @@ -1452,6 +1153,7 @@ item-level hold request. An item is available if * it is not lost AND * it is not damaged AND * it is not withdrawn AND +* a waiting or in transit reserve is placed on * does not have a not for loan value > 0 Need to check the issuingrules onshelfholds column, @@ -1474,10 +1176,12 @@ sub IsAvailableForItemLevelRequest { # FIXME - a lot of places in the code do this # or something similar - need to be # consolidated - my $itype = _get_itype($item); + my $patron = Koha::Patrons->find( $borrower->{borrowernumber} ); + my $item_object = Koha::Items->find( $item->{itemnumber } ); + my $itemtype = $item_object->effective_itemtype; my $notforloan_per_itemtype = $dbh->selectrow_array("SELECT notforloan FROM itemtypes WHERE itemtype = ?", - undef, $itype); + undef, $itemtype); return 0 if $notforloan_per_itemtype || @@ -1486,26 +1190,37 @@ sub IsAvailableForItemLevelRequest { $item->{withdrawn} || ($item->{damaged} && !C4::Context->preference('AllowHoldsOnDamagedItems')); + my $on_shelf_holds = Koha::IssuingRules->get_onshelfholds_policy( { item => $item_object, patron => $patron } ); - return 1 if _OnShelfHoldsAllowed($itype,$borrower->{categorycode},$item->{holdingbranch}); - - return $item->{onloan} || GetReserveStatus($item->{itemnumber}) eq "Waiting"; -} - -=head2 OnShelfHoldsAllowed + if ( $on_shelf_holds == 1 ) { + return 1; + } elsif ( $on_shelf_holds == 2 ) { + my @items = + Koha::Items->search( { biblionumber => $item->{biblionumber} } ); - OnShelfHoldsAllowed($itemtype,$borrowercategory,$branchcode); + my $any_available = 0; -Checks issuingrules, using the borrowers categorycode, the itemtype, and branchcode to see if onshelf -holds are allowed, returns true if so. + foreach my $i (@items) { -=cut + my $circ_control_branch = C4::Circulation::_GetCircControlBranch( $i->unblessed(), $borrower ); + my $branchitemrule = C4::Circulation::GetBranchItemRule( $circ_control_branch, $i->itype ); -sub OnShelfHoldsAllowed { - my ($item, $borrower) = @_; + $any_available = 1 + unless $i->itemlost + || $i->notforloan > 0 + || $i->withdrawn + || $i->onloan + || IsItemOnHoldAndFound( $i->id ) + || ( $i->damaged + && !C4::Context->preference('AllowHoldsOnDamagedItems') ) + || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan + || $branchitemrule->{holdallowed} == 1 && $borrower->{branchcode} ne $i->homebranch; + } - my $itype = _get_itype($item); - return _OnShelfHoldsAllowed($itype,$borrower->{categorycode},$item->{holdingbranch}); + return $any_available ? 0 : 1; + } else { # on_shelf_holds == 0 "If any unavailable" (the description is rather cryptic and could still be improved) + return $item->{onloan} || IsItemOnHoldAndFound( $item->{itemnumber} ); + } } sub _get_itype { @@ -1535,16 +1250,9 @@ sub _get_itype { return $itype; } -sub _OnShelfHoldsAllowed { - my ($itype,$borrowercategory,$branchcode) = @_; - - my $rule = C4::Circulation::GetIssuingRule($borrowercategory, $itype, $branchcode); - return $rule->{onshelfholds}; -} - =head2 AlterPriority - AlterPriority( $where, $reserve_id ); + AlterPriority( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority ); This function changes a reserve's priority up, down, to the top, or to the bottom. Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was placed @@ -1552,32 +1260,29 @@ Input: $where is 'up', 'down', 'top' or 'bottom'. Biblionumber, Date reserve was =cut sub AlterPriority { - my ( $where, $reserve_id ) = @_; - - my $dbh = C4::Context->dbh; + my ( $where, $reserve_id, $prev_priority, $next_priority, $first_priority, $last_priority ) = @_; - my $reserve = GetReserve( $reserve_id ); + my $hold = Koha::Holds->find( $reserve_id ); + return unless $hold; - if ( $reserve->{cancellationdate} ) { - warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (".$reserve->{cancellationdate}.')'; + if ( $hold->cancellationdate ) { + warn "I cannot alter the priority for reserve_id $reserve_id, the reserve has been cancelled (" . $hold->cancellationdate . ')'; return; } - if ( $where eq 'up' || $where eq 'down' ) { - - my $priority = $reserve->{'priority'}; - $priority = $where eq 'up' ? $priority - 1 : $priority + 1; - _FixPriority({ reserve_id => $reserve_id, rank => $priority }) - + if ( $where eq 'up' ) { + return unless $prev_priority; + _FixPriority({ reserve_id => $reserve_id, rank => $prev_priority }) + } elsif ( $where eq 'down' ) { + return unless $next_priority; + _FixPriority({ reserve_id => $reserve_id, rank => $next_priority }) } elsif ( $where eq 'top' ) { - - _FixPriority({ reserve_id => $reserve_id, rank => '1' }) - + _FixPriority({ reserve_id => $reserve_id, rank => $first_priority }) } elsif ( $where eq 'bottom' ) { - - _FixPriority({ reserve_id => $reserve_id, rank => '999999' }); - + _FixPriority({ reserve_id => $reserve_id, rank => $last_priority }); } + + # FIXME Should return the new priority } =head2 ToggleLowestPriority @@ -1595,7 +1300,7 @@ sub ToggleLowestPriority { my $sth = $dbh->prepare( "UPDATE reserves SET lowestPriority = NOT lowestPriority WHERE reserve_id = ?"); $sth->execute( $reserve_id ); - + _FixPriority({ reserve_id => $reserve_id, rank => '999999' }); } @@ -1612,29 +1317,15 @@ be cleared when it is unsuspended. sub ToggleSuspend { my ( $reserve_id, $suspend_until ) = @_; - $suspend_until = output_pref( - { - dt => dt_from_string($suspend_until), - dateformat => 'iso', - dateonly => 1 - } - ) if ($suspend_until); - - my $do_until = ( $suspend_until ) ? '?' : 'NULL'; + $suspend_until = dt_from_string($suspend_until) if ($suspend_until); - my $dbh = C4::Context->dbh; - - my $sth = $dbh->prepare( - "UPDATE reserves SET suspend = NOT suspend, - suspend_until = CASE WHEN suspend = 0 THEN NULL ELSE $do_until END - WHERE reserve_id = ? - "); + my $hold = Koha::Holds->find( $reserve_id ); - my @params; - push( @params, $suspend_until ) if ( $suspend_until ); - push( @params, $reserve_id ); - - $sth->execute( @params ); + if ( $hold->is_suspended ) { + $hold->resume() + } else { + $hold->suspend_hold( $suspend_until ); + } } =head2 SuspendAll @@ -1659,38 +1350,26 @@ sub SuspendAll { my $borrowernumber = $params{'borrowernumber'} || undef; my $biblionumber = $params{'biblionumber'} || undef; my $suspend_until = $params{'suspend_until'} || undef; - my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1; + my $suspend = defined( $params{'suspend'} ) ? $params{'suspend'} : 1; - $suspend_until = eval { output_pref( { dt => dt_from_string( $suspend_until), dateonly => 1, dateformat => 'iso' } ); } - if ( defined( $suspend_until ) ); + $suspend_until = eval { dt_from_string($suspend_until) } + if ( defined($suspend_until) ); return unless ( $borrowernumber || $biblionumber ); - my ( $query, $sth, $dbh, @query_params ); + my $params; + $params->{found} = undef; + $params->{borrowernumber} = $borrowernumber if $borrowernumber; + $params->{biblionumber} = $biblionumber if $biblionumber; - $query = "UPDATE reserves SET suspend = ? "; - push( @query_params, $suspend ); - if ( !$suspend ) { - $query .= ", suspend_until = NULL "; - } elsif ( $suspend_until ) { - $query .= ", suspend_until = ? "; - push( @query_params, $suspend_until ); - } - $query .= " WHERE "; - if ( $borrowernumber ) { - $query .= " borrowernumber = ? "; - push( @query_params, $borrowernumber ); + my @holds = Koha::Holds->search($params); + + if ($suspend) { + map { $_->suspend_hold($suspend_until) } @holds; } - $query .= " AND " if ( $borrowernumber && $biblionumber ); - if ( $biblionumber ) { - $query .= " biblionumber = ? "; - push( @query_params, $biblionumber ); + else { + map { $_->resume() } @holds; } - $query .= " AND found IS NULL "; - - $dbh = C4::Context->dbh; - $sth = $dbh->prepare( $query ); - $sth->execute( @query_params ); } @@ -1738,13 +1417,18 @@ sub _FixPriority { my $dbh = C4::Context->dbh; - unless ( $biblionumber ) { - my $res = GetReserve( $reserve_id ); - $biblionumber = $res->{biblionumber}; + my $hold; + if ( $reserve_id ) { + $hold = Koha::Holds->find( $reserve_id ); + return unless $hold; } - if ( $rank eq "del" ) { - CancelReserve({ reserve_id => $reserve_id }); + unless ( $biblionumber ) { # FIXME This is a very weird API + $biblionumber = $hold->biblionumber; + } + + if ( $rank eq "del" ) { # FIXME will crash if called without $hold + $hold->cancel; } elsif ( $rank eq "W" || $rank eq "0" ) { @@ -1805,7 +1489,7 @@ sub _FixPriority { $priority[$j]->{'reserve_id'} ); } - + $sth = $dbh->prepare( "SELECT reserve_id FROM reserves WHERE lowestPriority = 1 ORDER BY priority" ); $sth->execute(); @@ -1853,7 +1537,8 @@ sub _Findgroupreserve { reserves.timestamp AS timestamp, biblioitems.biblioitemnumber AS biblioitemnumber, reserves.itemnumber AS itemnumber, - reserves.reserve_id AS reserve_id + reserves.reserve_id AS reserve_id, + reserves.itemtype AS itemtype FROM reserves JOIN biblioitems USING (biblionumber) JOIN hold_fill_targets USING (biblionumber, borrowernumber, itemnumber) @@ -1887,7 +1572,8 @@ sub _Findgroupreserve { reserves.timestamp AS timestamp, biblioitems.biblioitemnumber AS biblioitemnumber, reserves.itemnumber AS itemnumber, - reserves.reserve_id AS reserve_id + reserves.reserve_id AS reserve_id, + reserves.itemtype AS itemtype FROM reserves JOIN biblioitems USING (biblionumber) JOIN hold_fill_targets USING (biblionumber, borrowernumber) @@ -1920,7 +1606,8 @@ sub _Findgroupreserve { reserves.priority AS priority, reserves.timestamp AS timestamp, reserves.itemnumber AS itemnumber, - reserves.reserve_id AS reserve_id + reserves.reserve_id AS reserve_id, + reserves.itemtype AS itemtype FROM reserves WHERE reserves.biblionumber = ? AND (reserves.itemnumber IS NULL OR reserves.itemnumber = ?) @@ -1940,7 +1627,7 @@ sub _Findgroupreserve { =head2 _koha_notify_reserve - _koha_notify_reserve( $itemnumber, $borrowernumber, $biblionumber ); + _koha_notify_reserve( $hold->reserve_id ); Sends a notification to the patron that their hold has been filled (through ModReserveAffect, _not_ ModReserveFill) @@ -1967,43 +1654,36 @@ The following tables are availalbe witin the notice: =cut sub _koha_notify_reserve { - my ($itemnumber, $borrowernumber, $biblionumber) = @_; + my $reserve_id = shift; + my $hold = Koha::Holds->find($reserve_id); + my $borrowernumber = $hold->borrowernumber; - my $dbh = C4::Context->dbh; - my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber); + my $patron = Koha::Patrons->find( $borrowernumber ); # Try to get the borrower's email address - my $to_address = C4::Members::GetNoticeEmailAddress($borrowernumber); + my $to_address = $patron->notice_email_address; my $messagingprefs = C4::Members::Messaging::GetMessagingPreferences( { borrowernumber => $borrowernumber, message_name => 'Hold_Filled' } ); - my $sth = $dbh->prepare(" - SELECT * - FROM reserves - WHERE borrowernumber = ? - AND biblionumber = ? - "); - $sth->execute( $borrowernumber, $biblionumber ); - my $reserve = $sth->fetchrow_hashref; - my $branch_details = GetBranchDetail( $reserve->{'branchcode'} ); + my $library = Koha::Libraries->find( $hold->branchcode )->unblessed; - my $admin_email_address = $branch_details->{'branchemail'} || C4::Context->preference('KohaAdminEmailAddress'); + my $admin_email_address = $library->{branchemail} || C4::Context->preference('KohaAdminEmailAddress'); my %letter_params = ( module => 'reserves', - branchcode => $reserve->{branchcode}, + branchcode => $hold->branchcode, + lang => $patron->lang, tables => { - 'branches' => $branch_details, - 'borrowers' => $borrower, - 'biblio' => $biblionumber, - 'biblioitems' => $biblionumber, - 'reserves' => $reserve, - 'items', $reserve->{'itemnumber'}, + 'branches' => $library, + 'borrowers' => $patron->unblessed, + 'biblio' => $hold->biblionumber, + 'biblioitems' => $hold->biblionumber, + 'reserves' => $hold->unblessed, + 'items' => $hold->itemnumber, }, - substitute => { today => output_pref( { dt => dt_from_string, dateonly => 1 } ) }, ); 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. @@ -2029,7 +1709,7 @@ sub _koha_notify_reserve { while ( my ( $mtt, $letter_code ) = each %{ $messagingprefs->{transports} } ) { next if ( ( $mtt eq 'email' and not $to_address ) # No email address - or ( $mtt eq 'sms' and not $borrower->{smsalertnumber} ) # No SMS number + or ( $mtt eq 'sms' and not $patron->smsalertnumber ) # No SMS number or ( $mtt eq 'phone' and C4::Context->preference('TalkingTechItivaPhoneNotification') ) # Notice is handled by TalkingTech_itiva_outbound.pl ); @@ -2040,7 +1720,7 @@ sub _koha_notify_reserve { if (! $notification_sent) { &$send_notification('print', 'HOLD'); } - + } =head2 _ShiftPriorityByDateAndPriority @@ -2092,54 +1772,6 @@ sub _ShiftPriorityByDateAndPriority { return $new_priority; # so the caller knows what priority they wind up receiving } -=head2 OPACItemHoldsAllowed - - OPACItemHoldsAllowed($item_record,$borrower_record); - -Checks issuingrules, using the borrowers categorycode, the itemtype, and branchcode to see -if specific item holds are allowed, returns true if so. - -=cut - -sub OPACItemHoldsAllowed { - my ($item,$borrower) = @_; - - my $branchcode = $item->{homebranch} or die "No homebranch"; - my $itype; - my $dbh = C4::Context->dbh; - if (C4::Context->preference('item-level_itypes')) { - # We can't trust GetItem to honour the syspref, so safest to do it ourselves - # When GetItem is fixed, we can remove this - $itype = $item->{itype}; - } - else { - my $query = "SELECT itemtype FROM biblioitems WHERE biblioitemnumber = ? "; - my $sth = $dbh->prepare($query); - $sth->execute($item->{biblioitemnumber}); - if (my $data = $sth->fetchrow_hashref()){ - $itype = $data->{itemtype}; - } - } - - my $query = "SELECT opacitemholds,categorycode,itemtype,branchcode FROM issuingrules WHERE - (issuingrules.categorycode = ? OR issuingrules.categorycode = '*') - AND - (issuingrules.itemtype = ? OR issuingrules.itemtype = '*') - AND - (issuingrules.branchcode = ? OR issuingrules.branchcode = '*') - ORDER BY - issuingrules.categorycode desc, - issuingrules.itemtype desc, - issuingrules.branchcode desc - LIMIT 1"; - my $sth = $dbh->prepare($query); - $sth->execute($borrower->{categorycode},$itype,$branchcode); - my $data = $sth->fetchrow_hashref; - my $opacitemholds = uc substr ($data->{opacitemholds}, 0, 1); - return '' if $opacitemholds eq 'N'; - return $opacitemholds; -} - =head2 MoveReserve MoveReserve( $itemnumber, $borrowernumber, $cancelreserve ) @@ -2157,7 +1789,6 @@ sub MoveReserve { return unless $res; my $biblionumber = $res->{biblionumber}; - my $biblioitemnumber = $res->{biblioitemnumber}; if ($res->{borrowernumber} == $borrowernumber) { ModReserveFill($res); @@ -2185,7 +1816,8 @@ sub MoveReserve { RevertWaitingStatus({ itemnumber => $itemnumber }); } elsif ( $cancelreserve eq 'cancel' || $cancelreserve ) { # cancel reserves on this item - CancelReserve( { reserve_id => $res->{'reserve_id'} } ); + my $hold = Koha::Holds->find( $res->{reserve_id} ); + $hold->cancel; } } } @@ -2295,48 +1927,21 @@ sub RevertWaitingStatus { _FixPriority( { biblionumber => $reserve->{biblionumber} } ); } -=head2 GetReserveId - - $reserve_id = GetReserveId({ biblionumber => $biblionumber, borrowernumber => $borrowernumber [, itemnumber => $itemnumber ] }); - - Returnes the first reserve id that matches the given criteria - -=cut - -sub GetReserveId { - my ( $params ) = @_; - - return unless ( ( $params->{'biblionumber'} || $params->{'itemnumber'} ) && $params->{'borrowernumber'} ); - - my $dbh = C4::Context->dbh(); - - my $sql = "SELECT reserve_id FROM reserves WHERE "; - - my @params; - my @limits; - foreach my $key ( keys %$params ) { - if ( defined( $params->{$key} ) ) { - push( @limits, "$key = ?" ); - push( @params, $params->{$key} ); - } - } - - $sql .= join( " AND ", @limits ); - - my $sth = $dbh->prepare( $sql ); - $sth->execute( @params ); - my $row = $sth->fetchrow_hashref(); - - return $row->{'reserve_id'}; -} - =head2 ReserveSlip - ReserveSlip($branchcode, $borrowernumber, $biblionumber) +ReserveSlip( + { + branchcode => $branchcode, + borrowernumber => $borrowernumber, + biblionumber => $biblionumber, + [ itemnumber => $itemnumber, ] + [ barcode => $barcode, ] + } + ) Returns letter hash ( see C4::Letters::GetPreparedLetter ) or undef -The letter code will be RESERVESLIP, and the following tables are +The letter code will be HOLD_SLIP, and the following tables are available within the slip: reserves @@ -2349,20 +1954,45 @@ available within the slip: =cut sub ReserveSlip { - my ($branch, $borrowernumber, $biblionumber) = @_; + my ($args) = @_; + my $branchcode = $args->{branchcode}; + my $borrowernumber = $args->{borrowernumber}; + my $biblionumber = $args->{biblionumber}; + my $itemnumber = $args->{itemnumber}; + my $barcode = $args->{barcode}; + + + my $patron = Koha::Patrons->find($borrowernumber); -# return unless ( C4::Context->boolean_preference('printreserveslips') ); + my $hold; + if ($itemnumber || $barcode ) { + $itemnumber ||= Koha::Items->find( { barcode => $barcode } )->itemnumber; + + $hold = Koha::Holds->search( + { + biblionumber => $biblionumber, + borrowernumber => $borrowernumber, + itemnumber => $itemnumber + } + )->next; + } + else { + $hold = Koha::Holds->search( + { + biblionumber => $biblionumber, + borrowernumber => $borrowernumber + } + )->next; + } - my $reserve_id = GetReserveId({ - biblionumber => $biblionumber, - borrowernumber => $borrowernumber - }) or return; - my $reserve = GetReserveInfo($reserve_id) or return; + return unless $hold; + my $reserve = $hold->unblessed; return C4::Letters::GetPreparedLetter ( module => 'circulation', - letter_code => 'RESERVESLIP', - branchcode => $branch, + letter_code => 'HOLD_SLIP', + branchcode => $branchcode, + lang => $patron->lang, tables => { 'reserves' => $reserve, 'branches' => $reserve->{branchcode}, @@ -2469,6 +2099,76 @@ sub IsItemOnHoldAndFound { return $found; } +=head2 GetMaxPatronHoldsForRecord + +my $holds_per_record = ReservesControlBranch( $borrowernumber, $biblionumber ); + +For multiple holds on a given record for a given patron, the max +number of record level holds that a patron can be placed is the highest +value of the holds_per_record rule for each item if the record for that +patron. This subroutine finds and returns the highest holds_per_record +rule value for a given patron id and record id. + +=cut + +sub GetMaxPatronHoldsForRecord { + my ( $borrowernumber, $biblionumber ) = @_; + + my $patron = Koha::Patrons->find($borrowernumber); + my @items = Koha::Items->search( { biblionumber => $biblionumber } ); + + my $controlbranch = C4::Context->preference('ReservesControlBranch'); + + my $categorycode = $patron->categorycode; + my $branchcode; + $branchcode = $patron->branchcode if ( $controlbranch eq "PatronLibrary" ); + + my $max = 0; + foreach my $item (@items) { + my $itemtype = $item->effective_itemtype(); + + $branchcode = $item->homebranch if ( $controlbranch eq "ItemHomeLibrary" ); + + my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode ); + my $holds_per_record = $rule ? $rule->{holds_per_record} : 0; + $max = $holds_per_record if $holds_per_record > $max; + } + + return $max; +} + +=head2 GetHoldRule + +my $rule = GetHoldRule( $categorycode, $itemtype, $branchcode ); + +Returns the matching hold related issuingrule fields for a given +patron category, itemtype, and library. + +=cut + +sub GetHoldRule { + my ( $categorycode, $itemtype, $branchcode ) = @_; + + my $dbh = C4::Context->dbh; + + my $sth = $dbh->prepare( + q{ + SELECT categorycode, itemtype, branchcode, reservesallowed, holds_per_record, holds_per_day + FROM issuingrules + WHERE (categorycode in (?,'*') ) + AND (itemtype IN (?,'*')) + AND (branchcode IN (?,'*')) + ORDER BY categorycode DESC, + itemtype DESC, + branchcode DESC + } + ); + + $sth->execute( $categorycode, $itemtype, $branchcode ); + + return $sth->fetchrow_hashref(); +} + =head1 AUTHOR Koha Development Team