From 7d9ba257669a1d7f803074d17daf5aac661383c2 Mon Sep 17 00:00:00 2001 From: Kyle M Hall Date: Thu, 14 Jun 2018 13:36:51 +0000 Subject: [PATCH] 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 Signed-off-by: Martin Renvoize Signed-off-by: Tomas Cohen Arazi --- C4/Circulation.pm | 35 +++++- Koha/ItemType.pm | 33 ++++++ admin/itemtypes.pl | 30 ++--- catalogue/moredetail.pl | 1 - .../prog/en/modules/admin/itemtypes.tt | 22 +++- .../prog/en/modules/catalogue/moredetail.tt | 4 +- t/db_dependent/Circulation.t | 105 +++++++++++++++++- t/db_dependent/Koha/ItemTypes.t | 52 ++++++++- 8 files changed, 250 insertions(+), 32 deletions(-) diff --git a/C4/Circulation.pm b/C4/Circulation.pm index fa082104a1..944d6c2876 100644 --- a/C4/Circulation.pm +++ b/C4/Circulation.pm @@ -977,6 +977,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; } @@ -1419,12 +1421,22 @@ sub AddIssue { ModDateLastSeen( $item->{'itemnumber'} ); # If it costs to borrow this book, charge it to the patron's account. - my ( $charge, $itemtype ) = GetIssuingCharges( $item->{'itemnumber'}, $borrower->{'borrowernumber'} ); + my ( $charge ) = GetIssuingCharges( $item->{'itemnumber'}, $borrower->{'borrowernumber'} ); if ( $charge > 0 ) { AddIssuingCharge( $issue, $charge ); $item->{'charge'} = $charge; } + 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( { @@ -2870,7 +2882,7 @@ 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 $accountno = C4::Accounts::getnextacctno( $borrowernumber ); @@ -2895,6 +2907,17 @@ sub AddRenewal { )->store(); } + # 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'; @@ -3212,12 +3235,14 @@ sub _get_discount_from_rule { =head2 AddIssuingCharge - &AddIssuingCharge( $checkout, $charge ) + &AddIssuingCharge( $checkout, $charge, [$type] ) =cut sub AddIssuingCharge { - my ( $checkout, $charge ) = @_; + my ( $checkout, $charge, $type ) = @_; + + $type ||= 'Rental'; # FIXME What if checkout does not exist? @@ -3238,7 +3263,7 @@ sub AddIssuingCharge { amountoutstanding => $charge, manager_id => $manager_id, branchcode => $branchcode, - description => 'Rental', + description => $type, accounttype => 'Rent', date => \'NOW()', } diff --git a/Koha/ItemType.pm b/Koha/ItemType.pm index 3d769a5f91..5faf499a69 100644 --- a/Koha/ItemType.pm +++ b/Koha/ItemType.pm @@ -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 diff --git a/admin/itemtypes.pl b/admin/itemtypes.pl index d397fee4f9..723dfe9c93 100755 --- a/admin/itemtypes.pl +++ b/admin/itemtypes.pl @@ -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; }; diff --git a/catalogue/moredetail.pl b/catalogue/moredetail.pl index ba599370e3..2d92cfc65e 100755 --- a/catalogue/moredetail.pl +++ b/catalogue/moredetail.pl @@ -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'} = sprintf( "%.2f", $data->{'rentalcharge'} || 0); # Price formatting should be done template-side foreach ( keys %{$data} ) { $template->param( "$_" => defined $data->{$_} ? $data->{$_} : '' ); } diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/itemtypes.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/itemtypes.tt index 84180cbf5e..6c8b0e71ba 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/itemtypes.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/itemtypes.tt @@ -138,7 +138,7 @@ Item types administration [% END %] [% END %] - (Options are defined as the authorized values for the ITEMTYPECAT category) + Options are defined as the authorized values for the ITEMTYPECAT category. [% IF Koha.Preference('noItemTypeImages') %]
  • @@ -218,7 +218,7 @@ Item types administration [% ELSE %] [% END %] - (if checked, items of this type will be hidden as filters in OPAC's advanced search) + If checked, items of this type will be hidden as filters in OPAC's advanced search.
  • @@ -227,11 +227,17 @@ Item types administration [% ELSE %] [% 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) + 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.
  • - + + This fee is charged once per checkout/renewal per item +
  • +
  • + + + This fee is charged a checkout/renewal time for each day between the checkout/renewal date and due date.
  • @@ -330,7 +336,8 @@ Item types administration Search category Not for loan Hide in OPAC - Charge + Rental charge + Daily rental charge Default replacement cost Processing fee (when lost) Checkin message @@ -372,6 +379,11 @@ Item types administration [% itemtype.rentalcharge | $Price %] [% END %] + + [% UNLESS ( itemtype.notforloan ) %] + [% itemtype.rental_charge_daily | $Price %] + [% END %] + [% itemtype.defaultreplacecost | $Price %] [% itemtype.processfee | $Price %] [% itemtype.checkinmsg | html_line_break | $raw %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt index 4474432d93..183e78f949 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt @@ -1,4 +1,5 @@ [% USE raw %] +[% USE Price %] [% USE Asset %] [% USE Koha %] [% USE Branches %] @@ -32,7 +33,8 @@ [% UNLESS ( item_level_itypes ) %]
  • Item type: [% itemtypename | html %] 
  • [% END %] - [% IF ( rentalcharge ) %]
  • Rental charge:[% rentalcharge | html %] 
  • [% END %] + [% IF ( rentalcharge ) %]
  • Rental charge:[% rentalcharge | $Price %] 
  • [% END %] + [% IF ( rental_charge_daily ) %]
  • Daily rental charge:[% rental_charge_daily | $Price %] 
  • [% END %]
  • ISBN: [% isbn | html %] 
  • Publisher:[% place | html %] [% publishercode | html %] [% publicationyear | html %] 
  • [% IF ( volumeddesc ) %]
  • Volume: [% volumeddesc | html %]
  • [% END %] diff --git a/t/db_dependent/Circulation.t b/t/db_dependent/Circulation.t index 707582d44a..0f25c98fc3 100755 --- a/t/db_dependent/Circulation.t +++ b/t/db_dependent/Circulation.t @@ -68,8 +68,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( @@ -2980,4 +2987,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(); + +}; diff --git a/t/db_dependent/Koha/ItemTypes.t b/t/db_dependent/Koha/ItemTypes.t index eb08c65927..34ab094c9f 100755 --- a/t/db_dependent/Koha/ItemTypes.t +++ b/t/db_dependent/Koha/ItemTypes.t @@ -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; -- 2.20.1