@@ -, +, @@ circulation rules 1) Go to /cgi-bin/koha/admin/preferences.pl and set 1) Go to /cgi-bin/koha/admin/smart-rules.pl circulation rules table 2) In that row add numerical values to type/patron categories 3) Prepare an item for our next step, pick a biblio that has item 3) Place a hold on that item and set the expiration date as the 4) Check in it after that. 5) Do SQL request to change expiration date manually to "yesterday": Or alternatively: 5) Wait for tomorrow, change server time or invent a time machine. 6) Run cancel_expired_holds.pl cron job. 7) Check that patron got charged in "accounting" left tab and 8) You can repeat this with other rules and different 9) If there's no matching smart rule or smart rule have an empty 10) But in case if matching rule has zero ("0") as a value - that --- Koha/CirculationRules.pm | 3 + Koha/Hold.pm | 20 +- admin/smart-rules.pl | 3 + installer/data/mysql/updatedatabase.pl | 1 + .../prog/en/modules/admin/smart-rules.tt | 17 +- t/db_dependent/Holds/Holdfine.t | 186 ++++++++++++++++++ 6 files changed, 226 insertions(+), 4 deletions(-) create mode 100755 t/db_dependent/Holds/Holdfine.t --- a/Koha/CirculationRules.pm +++ a/Koha/CirculationRules.pm @@ -79,6 +79,9 @@ our $RULE_KINDS = { cap_fine_to_replacement_price => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], }, + expire_reserves_charge => { + scope => [ 'branchcode', 'categorycode', 'itemtype' ], + }, chargeperiod => { scope => [ 'branchcode', 'categorycode', 'itemtype' ], }, --- a/Koha/Hold.pm +++ a/Koha/Hold.pm @@ -372,7 +372,25 @@ sub cancel { C4::Reserves::_FixPriority({ biblionumber => $self->biblionumber }); # and, if desired, charge a cancel fee - my $charge = C4::Context->preference("ExpireReservesMaxPickUpDelayCharge"); + my $charge; + if (my $item = $self->item) { + my $branchcode = C4::Reserves::GetReservesControlBranch($item, $self->borrower); + # C4::Reserves::GetReservesControlBranch($item->unblessed, $self->borrower->unblessed); + + my $rule = Koha::CirculationRules->get_effective_rule( + { + categorycode => $self->borrower->categorycode, + itemtype => $item->effective_itemtype, + branchcode => $branchcode, + rule_name => 'expire_reserves_charge', + } + ); + my $rule_value = $rule && $rule->rule_value // ''; + $charge = $rule_value ne '' ? $rule_value : C4::Context->preference("ExpireReservesMaxPickUpDelayCharge"); + } else { + $charge = C4::Context->preference("ExpireReservesMaxPickUpDelayCharge"); + } + if ( $charge && $params->{'charge_cancel_fee'} ) { my $account = Koha::Account->new( { patron_id => $self->borrowernumber } ); --- a/admin/smart-rules.pl +++ a/admin/smart-rules.pl @@ -111,6 +111,7 @@ if ($op eq 'delete') { opacitemholds => undef, overduefinescap => undef, cap_fine_to_replacement_price => undef, + expire_reserves_charge => undef, article_requests => undef, note => undef, } @@ -288,6 +289,7 @@ elsif ($op eq 'add') { my $article_requests = $input->param('article_requests') || 'no'; my $overduefinescap = $input->param('overduefinescap') || ''; my $cap_fine_to_replacement_price = ($input->param('cap_fine_to_replacement_price') || '') eq 'on'; + my $expire_reserves_charge = $input->param('expire_reserves_charge') || ''; my $note = $input->param('note'); $debug and warn "Adding $br, $bor, $itemtype, $fine, $maxissueqty, $maxonsiteissueqty, $cap_fine_to_replacement_price"; @@ -320,6 +322,7 @@ elsif ($op eq 'add') { opacitemholds => $opacitemholds, overduefinescap => $overduefinescap, cap_fine_to_replacement_price => $cap_fine_to_replacement_price, + expire_reserves_charge => $expire_reserves_charge, article_requests => $article_requests, note => $note, }; --- a/installer/data/mysql/updatedatabase.pl +++ a/installer/data/mysql/updatedatabase.pl @@ -20696,6 +20696,7 @@ if( CheckVersion( $DBversion ) ) { auto_renew no_auto_renewal_after no_auto_renewal_after_hard_limit + expire_reserves_charge reservesallowed holds_per_record holds_per_day --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/smart-rules.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/smart-rules.tt @@ -102,6 +102,7 @@ Fine grace period Overdue fines cap (amount) Cap fine at replacement price + Expire reserve charge Suspension in days (day) Max. suspension duration (day) Suspension charging interval @@ -141,6 +142,7 @@ [% SET firstremind = all_rules.$c.$i.firstremind %] [% SET overduefinescap = all_rules.$c.$i.overduefinescap %] [% SET cap_fine_to_replacement_price = all_rules.$c.$i.cap_fine_to_replacement_price %] + [% SET expire_reserves_charge = all_rules.$c.$i.expire_reserves_charge %] [% SET finedays = all_rules.$c.$i.finedays %] [% SET maxsuspensiondays = all_rules.$c.$i.maxsuspensiondays %] [% SET suspension_chargeperiod = all_rules.$c.$i.suspension_chargeperiod %] @@ -158,7 +160,7 @@ [% SET article_requests = all_rules.$c.$i.article_requests %] [% SET rentaldiscount = all_rules.$c.$i.rentaldiscount %] - [% SET show_rule = note || maxissueqty || maxonsiteissueqty || issuelength || daysmode || lengthunit || hardduedate || hardduedatecompare || fine || chargeperiod || chargeperiod_charge_at || firstremind || overduefinescap || cap_fine_to_replacement_price || finedays || maxsuspensiondays || suspension_chargeperiod || renewalsallowed || renewalperiod || norenewalbefore || auto_renew || no_auto_renewal_after || no_auto_renewal_after_hard_limit || reservesallowed || holds_per_day || holds_per_record || onshelfholds || opacitemholds || article_requests || rentaldiscount %] + [% SET show_rule = note || maxissueqty || maxonsiteissueqty || issuelength || daysmode || lengthunit || hardduedate || hardduedatecompare || fine || chargeperiod || chargeperiod_charge_at || firstremind || overduefinescap || cap_fine_to_replacement_price || expire_reserves_charge || finedays || maxsuspensiondays || suspension_chargeperiod || renewalsallowed || renewalperiod || norenewalbefore || auto_renew || no_auto_renewal_after || no_auto_renewal_after_hard_limit || reservesallowed || holds_per_day || holds_per_record || onshelfholds || opacitemholds || article_requests || rentaldiscount %] [% IF show_rule %] [% SET row_count = row_count + 1 %] @@ -246,6 +248,13 @@ [% END %] + + [% IF expire_reserves_charge.defined && expire_reserves_charge != '' %] + [% expire_reserves_charge | html %] + [% ELSE %] + Use global + [% END %] + [% finedays | html %] [% maxsuspensiondays | html %] [% suspension_chargeperiod | html %] @@ -381,6 +390,7 @@ + @@ -448,6 +458,7 @@ Fine grace period Overdue fines cap (amount) Cap fine at replacement price + Expire reserve charge Suspension in days (day) Max. suspension duration (day) Suspension charging interval @@ -1020,7 +1031,7 @@ } $(current_column).find("input[type='text']").val(input_value); $(current_column).find("select").val(select_value); - } else if ( i == 15 ) { + } else if ( i == 16 ) { // specific processing for cap_fine_to_replacement_price var cap_fine_to_replacement_price = $(this).find("input[type='checkbox']"); $('#cap_fine_to_replacement_price').prop('checked', cap_fine_to_replacement_price.is(':checked') ); @@ -1045,7 +1056,7 @@ // Remove potential previous input added $(current_column).find("input").remove(); $(current_column).append(""); - } else if ( i == 5 || i == 6 || i == 25 || i == 26 || i == 27 ) { + } else if ( i == 5 || i == 6 || i == 26 || i == 27 || i == 28 ) { // If the value is not an integer for // - "Current checkouts allowed" // - "Current on-site checkouts allowed" --- a/t/db_dependent/Holds/Holdfine.t +++ a/t/db_dependent/Holds/Holdfine.t @@ -0,0 +1,186 @@ +#!/usr/bin/perl + +# Copyright The National Library of Finland, University of Helsinki 2020 +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, see . + +use Modern::Perl; + +use C4::Context; +use Koha::CirculationRules; + +use Test::More tests => 10; + +use t::lib::TestBuilder; +use t::lib::Mocks; +use Koha::Holds; + +use Koha::Account; +use Koha::Account::DebitTypes; + +BEGIN { + use_ok('C4::Reserves'); +} + +my $schema = Koha::Database->schema; +$schema->storage->txn_begin; +my $dbh = C4::Context->dbh; + +my $builder = t::lib::TestBuilder->new; + +my $library1 = $builder->build({ + source => 'Branch', +}); + +my $bib_title = "Test Title"; + +my $borrower = $builder->build({ + source => 'Borrower', + value => { + branchcode => $library1->{branchcode}, + } +}); + +my $borrowernumber = $borrower->{borrowernumber}; +my $library_A_code = $library1->{branchcode}; + +my $biblio = $builder->build_sample_biblio({itemtype=>'BK'}); +my $biblionumber = $biblio->biblionumber; +my $item1 = $builder->build_sample_item({ + biblionumber => $biblionumber, + itype => 'CF', + homebranch => $library_A_code, + holdingbranch => $library_A_code +}); +my $item2 = $builder->build_sample_item({ + biblionumber => $biblionumber, + itype => 'MU', + homebranch => $library_A_code, + holdingbranch => $library_A_code +}); +my $item3 = $builder->build_sample_item({ + biblionumber => $biblionumber, + itype => 'MX', + homebranch => $library_A_code, + holdingbranch => $library_A_code +}); + +$dbh->do("DELETE FROM circulation_rules"); +Koha::CirculationRules->set_rules( + { + itemtype => 'CF', + categorycode => undef, + branchcode => undef, + rules => { + expire_reserves_charge => '111' + } + } +); +Koha::CirculationRules->set_rules( + { + itemtype => 'MU', + categorycode => undef, + branchcode => undef, + rules => { + expire_reserves_charge => undef + } + } +); + +my $reserve_id; +my $account; +my $status; +my $start_balance; + +# TEST: Hold Computer File item +$reserve_id = AddReserve( + { + branchcode => $library_A_code, + borrowernumber => $borrowernumber, + biblionumber => $biblionumber, + priority => 1, + itemnumber => $item1->itemnumber, + } +); + +$account = Koha::Account->new({ patron_id => $borrowernumber }); + +( $status ) = CheckReserves($item1->id); +is( $status, 'Reserved', "Hold for the CF created" ); + +$start_balance = $account->balance(); + +Koha::Holds->find( $reserve_id )->cancel({ charge_cancel_fee => 1 }); + +( $status ) = CheckReserves($item1->id); +is( $status, '', "Hold for the CF cancelled" ); + +is( $account->balance() - $start_balance, 111, "Account debt is 111" ); + +# TEST: Hold Music item that has 'expire_reserves_charge' set undef +t::lib::Mocks::mock_preference('ExpireReservesMaxPickUpDelayCharge', 222); + +$reserve_id = AddReserve( + { + branchcode => $library_A_code, + borrowernumber => $borrowernumber, + biblionumber => $biblionumber, + priority => 1, + itemnumber => $item2->itemnumber, + } +); + +$account = Koha::Account->new({ patron_id => $borrowernumber }); + +( $status ) = CheckReserves($item2->id); +is( $status, 'Reserved', "Hold for the MU created" ); + +$start_balance = $account->balance(); + +Koha::Holds->find( $reserve_id )->cancel({ charge_cancel_fee => 1 }); + +( $status ) = CheckReserves($item2->id); +is( $status, '', "Hold for the MU cancelled" ); + +is( $account->balance() - $start_balance, 222, "Account debt is 222" ); + +# TEST: Hold MX item that has no circulation rules +t::lib::Mocks::mock_preference('ExpireReservesMaxPickUpDelayCharge', 333); + +$reserve_id = AddReserve( + { + branchcode => $library_A_code, + borrowernumber => $borrowernumber, + biblionumber => $biblionumber, + priority => 1, + itemnumber => $item3->itemnumber, + } +); + +$account = Koha::Account->new({ patron_id => $borrowernumber }); + +( $status ) = CheckReserves($item3->id); +is( $status, 'Reserved', "Hold for the MX created" ); + +$start_balance = $account->balance(); + +Koha::Holds->find( $reserve_id )->cancel({ charge_cancel_fee => 1 }); + +( $status ) = CheckReserves($item3->id); +is( $status, '', "Hold for the MX cancelled" ); + +is( $account->balance() - $start_balance, 333, "Account debt is 333" ); + +$schema->storage->txn_rollback; --