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:
parent
e39541522c
commit
3d84f4b9b3
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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; };
|
||||
|
|
|
|||
|
|
@ -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->{$_} : '' );
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %] </li>
|
||||
[% END %]
|
||||
[% IF ( rentalcharge ) %]<li><span class="label">Rental charge:</span>[% rentalcharge | $Price %] </li>[% END %]
|
||||
[% IF ( rental_charge_daily ) %]<li><span class="label">Daily rental charge:</span>[% rental_charge_daily | $Price %] </li>[% END %]
|
||||
<li><span class="label">ISBN:</span> [% isbn | html %] </li>
|
||||
<li><span class="label">Publisher:</span>[% place | html %] [% publishercode | html %] [% publicationyear | html %] </li>
|
||||
[% IF ( volumeddesc ) %]<li><span class="label">Volume:</span> [% volumeddesc | html %]</li>[% END %]
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue