Bug 20912: Rental Fees based on Time Period

Some libraries would like to be able to charge a rental fee based on the
number of days an item will be checked out, as opposed to the flat fee
currently offered by Koha.

Test Plan:
1) Apply this patch
2) Run updatedatabase.pl
3) Edit an itemtype, add a daily rental fee of 1.00
4) Check an item of that itemtype out for 7 days
5) Verify the patron now has rental fee of 7.00

Signed-off-by: Matha Fuerst <mfuerst@hmcpl.org>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
This commit is contained in:
Kyle M Hall 2018-06-14 13:36:51 +00:00 committed by Nick Clemens
parent e39541522c
commit 3d84f4b9b3
8 changed files with 244 additions and 28 deletions

View File

@ -989,6 +989,8 @@ sub CanBookBeIssued {
if ( $rentalConfirmation ){
my ($rentalCharge) = GetIssuingCharges( $item->itemnumber, $patron->borrowernumber );
my $itemtype = Koha::ItemTypes->find( $item->itype ); # GetItem sets effective itemtype
$rentalCharge += $itemtype->calc_rental_charge_daily( { from => dt_from_string(), to => $duedate } );
if ( $rentalCharge > 0 ){
$needsconfirmation{RENTALCHARGE} = $rentalCharge;
}
@ -1437,6 +1439,16 @@ sub AddIssue {
AddIssuingCharge( $issue, $charge, $description );
}
my $itemtype = Koha::ItemTypes->find( $item_object->effective_itemtype );
if ( $itemtype ) {
my $daily_charge = $itemtype->calc_rental_charge_daily( { from => $issuedate, to => $datedue } );
if ( $daily_charge > 0 ) {
AddIssuingCharge( $issue, $daily_charge, 'Daily rental' ) if $daily_charge > 0;
$charge += $daily_charge;
$item->{charge} = $charge;
}
}
# Record the fact that this book was issued.
&UpdateStats(
{
@ -2859,13 +2871,24 @@ sub AddRenewal {
$renews = $item->renewals + 1;
ModItem( { renewals => $renews, onloan => $datedue->strftime('%Y-%m-%d %H:%M')}, $item->biblionumber, $itemnumber, { log_action => 0 } );
# Charge a new rental fee, if applicable?
# Charge a new rental fee, if applicable
my ( $charge, $type ) = GetIssuingCharges( $itemnumber, $borrowernumber );
if ( $charge > 0 ) {
my $description = "Renewal of Rental Item " . $biblio->title . " " .$item->barcode;
AddIssuingCharge($issue, $charge, $description);
}
# Charge a new daily rental fee, if applicable
my $itemtype = Koha::ItemTypes->find( $item_object->effective_itemtype );
if ( $itemtype ) {
my $daily_charge = $itemtype->calc_rental_charge_daily( { from => dt_from_string($lastreneweddate), to => $datedue } );
if ( $daily_charge > 0 ) {
my $type_desc = "Renewal of Daily Rental Item " . $biblio->title . " $item->{'barcode'}";
AddIssuingCharge( $issue, $daily_charge, $type_desc )
}
$charge += $daily_charge;
}
# Send a renewal slip according to checkout alert preferencei
if ( C4::Context->preference('RenewalSendNotice') eq '1' ) {
my $circulation_alert = 'C4::ItemCirculationAlertPreference';
@ -3183,7 +3206,7 @@ sub _get_discount_from_rule {
=head2 AddIssuingCharge
&AddIssuingCharge( $checkout, $charge )
&AddIssuingCharge( $checkout, $charge, [$description] )
=cut

View File

@ -90,6 +90,39 @@ sub translated_descriptions {
} @translated_descriptions ];
}
=head3 calc_rental_charge_daily
my $fee = $itemtype->calc_rental_charge_daily( { from => $dt_from, to => $dt_to } );
This method calculates the daily rental fee for a given itemtype for a given
period of time passed in as a pair of DateTime objects.
=cut
sub calc_rental_charge_daily {
my ( $self, $params ) = @_;
my $rental_charge_daily = $self->rental_charge_daily;
return 0 unless $rental_charge_daily;
my $from_dt = $params->{from};
my $to_dt = $params->{to};
my $duration;
if ( C4::Context->preference('finesCalendar') eq 'noFinesWhenClosed' ) {
my $branchcode = C4::Context->userenv->{branch};
my $calendar = Koha::Calendar->new( branchcode => $branchcode );
$duration = $calendar->days_between( $from_dt, $to_dt );
}
else {
$duration = $to_dt->delta_days($from_dt);
}
my $days = $duration->in_units('days');
my $charge = $rental_charge_daily * $days;
return $charge;
}
=head3 can_be_deleted

View File

@ -72,6 +72,7 @@ if ( $op eq 'add_form' ) {
my $itemtype = Koha::ItemTypes->find($itemtype_code);
my $description = $input->param('description');
my $rentalcharge = $input->param('rentalcharge');
my $rental_charge_daily = $input->param('rental_charge_daily');
my $defaultreplacecost = $input->param('defaultreplacecost');
my $processfee = $input->param('processfee');
my $image = $input->param('image') || q||;
@ -92,6 +93,7 @@ if ( $op eq 'add_form' ) {
if ( $itemtype and $is_a_modif ) { # it's a modification
$itemtype->description($description);
$itemtype->rentalcharge($rentalcharge);
$itemtype->rental_charge_daily($rental_charge_daily);
$itemtype->defaultreplacecost($defaultreplacecost);
$itemtype->processfee($processfee);
$itemtype->notforloan($notforloan);
@ -112,19 +114,21 @@ if ( $op eq 'add_form' ) {
}
} elsif ( not $itemtype and not $is_a_modif ) {
my $itemtype = Koha::ItemType->new(
{ itemtype => $itemtype_code,
description => $description,
rentalcharge => $rentalcharge,
defaultreplacecost => $defaultreplacecost,
processfee => $processfee,
notforloan => $notforloan,
imageurl => $imageurl,
summary => $summary,
checkinmsg => $checkinmsg,
checkinmsgtype => $checkinmsgtype,
sip_media_type => $sip_media_type,
hideinopac => $hideinopac,
searchcategory => $searchcategory,
{
itemtype => $itemtype_code,
description => $description,
rentalcharge => $rentalcharge,
rental_charge_daily => $rental_charge_daily,
defaultreplacecost => $defaultreplacecost,
processfee => $processfee,
notforloan => $notforloan,
imageurl => $imageurl,
summary => $summary,
checkinmsg => $checkinmsg,
checkinmsgtype => $checkinmsgtype,
sip_media_type => $sip_media_type,
hideinopac => $hideinopac,
searchcategory => $searchcategory,
}
);
eval { $itemtype->store; };

View File

@ -128,7 +128,6 @@ my $itemtypes = { map { $_->{itemtype} => $_ } @{ Koha::ItemTypes->search_with_l
$data->{'itemtypename'} = $itemtypes->{ $data->{'itemtype'} }->{'translated_description'}
if $data->{itemtype} && exists $itemtypes->{ $data->{itemtype} };
$data->{'rentalcharge'} = $data->{'rentalcharge'};
foreach ( keys %{$data} ) {
$template->param( "$_" => defined $data->{$_} ? $data->{$_} : '' );
}

View File

@ -137,7 +137,7 @@ Item types administration
[% END %]
[% END %]
</select>
(Options are defined as the authorized values for the ITEMTYPECAT category)
<span class="hint">Options are defined as the authorized values for the ITEMTYPECAT category.</span>
</li>
[% IF Koha.Preference('noItemTypeImages') %]
<li>
@ -217,7 +217,7 @@ Item types administration
[% ELSE %]
<input type="checkbox" id="hideinopac" name="hideinopac" value="1" />
[% END %]
(if checked, items of this type will be hidden as filters in OPAC's advanced search)
<span class="hint">If checked, items of this type will be hidden as filters in OPAC's advanced search.</span>
</li>
<li>
<label for="notforloan">Not for loan: </label>
@ -226,11 +226,17 @@ Item types administration
[% ELSE %]
<input type="checkbox" id="notforloan" name="notforloan" value="1" />
[% END %]
(if checked, no item of this type can be issued. If not checked, every item of this type can be issued unless notforloan is set for a specific item)
<span class="hint">If checked, no item of this type can be issued. If not checked, every item of this type can be issued unless notforloan is set for a specific item.</span>
</li>
<li>
<label for="rentalcharge">Rental charge: </label>
<input type="text" id="rentalcharge" name="rentalcharge" size="10" value="[% itemtype.rentalcharge | html %]" />
<input type="text" id="rentalcharge" name="rentalcharge" size="10" value="[% itemtype.rentalcharge | $Price %]" />
<span class="hint">This fee is charged once per checkout/renewal per item</span>
</li>
<li>
<label for="rental_charge_daily">Daily rental charge: </label>
<input type="text" id="rental_charge_daily" name="rental_charge_daily" size="10" value="[% itemtype.rental_charge_daily | $Price %]" />
<span class="hint">This fee is charged a checkout/renewal time for each day between the checkout/renewal date and due date.</span>
</li>
<li>
<label for="defaultreplacecost">Default replacement cost: </label>
@ -329,7 +335,8 @@ Item types administration
<th>Search category</th>
<th>Not for loan</th>
<th>Hide in OPAC</th>
<th>Charge</th>
<th>Rental charge</th>
<th>Daily rental charge</th>
<th>Default replacement cost</th>
<th>Processing fee (when lost)</th>
<th>Checkin message</th>
@ -371,6 +378,11 @@ Item types administration
[% itemtype.rentalcharge | $Price %]
[% END %]
</td>
<td>
[% UNLESS ( itemtype.notforloan ) %]
[% itemtype.rental_charge_daily | $Price %]
[% END %]
</td>
<td>[% itemtype.defaultreplacecost | $Price %]</td>
<td>[% itemtype.processfee | $Price %]</td>
<td>[% itemtype.checkinmsg | html_line_break | $raw %]</td>

View File

@ -1,4 +1,5 @@
[% USE raw %]
[% USE Price %]
[% USE Asset %]
[% USE Koha %]
[% USE Branches %]
@ -34,6 +35,7 @@
<li><span class="label">Item type:</span> [% itemtypename | html %]&nbsp;</li>
[% END %]
[% IF ( rentalcharge ) %]<li><span class="label">Rental charge:</span>[% rentalcharge | $Price %]&nbsp;</li>[% END %]
[% IF ( rental_charge_daily ) %]<li><span class="label">Daily rental charge:</span>[% rental_charge_daily | $Price %]&nbsp;</li>[% END %]
<li><span class="label">ISBN:</span> [% isbn | html %]&nbsp;</li>
<li><span class="label">Publisher:</span>[% place | html %] [% publishercode | html %] [% publicationyear | html %]&nbsp;</li>
[% IF ( volumeddesc ) %]<li><span class="label">Volume:</span> [% volumeddesc | html %]</li>[% END %]

View File

@ -70,8 +70,15 @@ my $library2 = $builder->build({
source => 'Branch',
});
my $itemtype = $builder->build(
{ source => 'Itemtype',
value => { notforloan => undef, rentalcharge => 0, defaultreplacecost => undef, processfee => undef }
{
source => 'Itemtype',
value => {
notforloan => undef,
rentalcharge => 0,
rental_charge_daily => 0,
defaultreplacecost => undef,
processfee => undef
}
}
)->{itemtype};
my $patron_category = $builder->build(
@ -2993,4 +3000,96 @@ sub test_debarment_on_checkout {
$expected_expiration_date, 'Test at line ' . $line_number );
Koha::Patron::Debarments::DelUniqueDebarment(
{ borrowernumber => $patron->{borrowernumber}, type => 'SUSPENSION' } );
}
};
subtest 'Koha::ItemType::calc_rental_charge_daily tests' => sub {
plan tests => 8;
t::lib::Mocks::mock_preference('item-level_itypes', 1);
my $library = $builder->build_object( { class => 'Koha::Libraries' } )->store;
my $module = new Test::MockModule('C4::Context');
$module->mock('userenv', sub { { branch => $library->id } });
my $patron = $builder->build_object(
{
class => 'Koha::Patrons',
value => { categorycode => $patron_category->{categorycode} }
}
)->store;
my $itemtype = $builder->build_object(
{
class => 'Koha::ItemTypes',
value => {
notforloan => undef,
rentalcharge => 0,
rental_charge_daily => 1.000000
}
}
)->store;
my $biblioitem = $builder->build( { source => 'Biblioitem' } );
my $item = $builder->build_object(
{
class => 'Koha::Items',
value => {
homebranch => $library->id,
holdingbranch => $library->id,
notforloan => 0,
itemlost => 0,
withdrawn => 0,
itype => $itemtype->id,
biblionumber => $biblioitem->{biblionumber},
biblioitemnumber => $biblioitem->{biblioitemnumber},
}
}
)->store;
is( $itemtype->rental_charge_daily, '1.000000', 'Daily rental charge stored and retreived correctly' );
is( $item->effective_itemtype, $itemtype->id, "Itemtype set correctly for item");
my $dt_from = dt_from_string();
my $dt_to = dt_from_string()->add( days => 7 );
my $dt_to_renew = dt_from_string()->add( days => 13 );
t::lib::Mocks::mock_preference('finesCalendar', 'ignoreCalendar');
my $issue = AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
my $accountline = Koha::Account::Lines->find({ itemnumber => $item->id });
is( $accountline->amount, '7.000000', "Daily rental charge calulated correctly with finesCalendar = ignoreCalendar" );
$accountline->delete();
AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
$accountline = Koha::Account::Lines->find({ itemnumber => $item->id });
is( $accountline->amount, '6.000000', "Daily rental charge calulated correctly with finesCalendar = ignoreCalendar, for renewal" );
$accountline->delete();
$issue->delete();
t::lib::Mocks::mock_preference('finesCalendar', 'noFinesWhenClosed');
$issue = AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
$accountline = Koha::Account::Lines->find({ itemnumber => $item->id });
is( $accountline->amount, '7.000000', "Daily rental charge calulated correctly with finesCalendar = noFinesWhenClosed" );
$accountline->delete();
AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
$accountline = Koha::Account::Lines->find({ itemnumber => $item->id });
is( $accountline->amount, '6.000000', "Daily rental charge calulated correctly with finesCalendar = noFinesWhenClosed, for renewal" );
$accountline->delete();
$issue->delete();
my $calendar = C4::Calendar->new( branchcode => $library->id );
$calendar->insert_week_day_holiday(
weekday => 3,
title => 'Test holiday',
description => 'Test holiday'
);
$issue = AddIssue( $patron->unblessed, $item->barcode, $dt_to, undef, $dt_from );
$accountline = Koha::Account::Lines->find({ itemnumber => $item->id });
is( $accountline->amount, '6.000000', "Daily rental charge calulated correctly with finesCalendar = noFinesWhenClosed and closed Wednesdays" );
$accountline->delete();
AddRenewal( $patron->id, $item->id, $library->id, $dt_to_renew, $dt_to );
$accountline = Koha::Account::Lines->find({ itemnumber => $item->id });
is( $accountline->amount, '5.000000', "Daily rental charge calulated correctly with finesCalendar = noFinesWhenClosed and closed Wednesdays, for renewal" );
$accountline->delete();
$issue->delete();
};

View File

@ -19,14 +19,19 @@
use Modern::Perl;
use Test::More tests => 24;
use Data::Dumper;
use Koha::Database;
use Test::More tests => 25;
use t::lib::Mocks;
use Koha::Items;
use Koha::Biblioitems;
use t::lib::TestBuilder;
use C4::Calendar;
use Koha::Biblioitems;
use Koha::Libraries;
use Koha::Database;
use Koha::DateUtils qw(dt_from_string);;
use Koha::Items;
BEGIN {
use_ok('Koha::ItemType');
use_ok('Koha::ItemTypes');
@ -144,4 +149,43 @@ $biblioitem->delete;
is ( $item_type->can_be_deleted, 1, 'The item type that was being used by the removed item and biblioitem can now be deleted' );
subtest 'Koha::ItemType::calc_rental_charge_daily tests' => sub {
plan tests => 4;
my $library = Koha::Libraries->search()->next();
my $module = new Test::MockModule('C4::Context');
$module->mock('userenv', sub { { branch => $library->id } });
my $itemtype = Koha::ItemType->new(
{
itemtype => 'type4',
description => 'description',
rental_charge_daily => 1.00,
}
)->store;
is( $itemtype->rental_charge_daily, 1.00, 'Daily rental charge stored and retreived correctly' );
my $dt_from = dt_from_string();
my $dt_to = dt_from_string()->add( days => 7 );
t::lib::Mocks::mock_preference('finesCalendar', 'ignoreCalendar');
my $charge = $itemtype->calc_rental_charge_daily( { from => $dt_from, to => $dt_to } );
is( $charge, 7.00, "Daily rental charge calulated correctly with finesCalendar = ignoreCalendar" );
t::lib::Mocks::mock_preference('finesCalendar', 'noFinesWhenClosed');
$charge = $itemtype->calc_rental_charge_daily( { from => $dt_from, to => $dt_to } );
is( $charge, 7.00, "Daily rental charge calulated correctly with finesCalendar = noFinesWhenClosed" );
my $calendar = C4::Calendar->new( branchcode => $library->id );
$calendar->insert_week_day_holiday(
weekday => 3,
title => 'Test holiday',
description => 'Test holiday'
);
$charge = $itemtype->calc_rental_charge_daily( { from => $dt_from, to => $dt_to } );
is( $charge, 6.00, "Daily rental charge calulated correctly with finesCalendar = noFinesWhenClosed and closed Wednesdays" );
};
$schema->txn_rollback;