Bug 20912: Rental Fees based on Time Period
authorKyle M Hall <kyle@bywatersolutions.com>
Thu, 14 Jun 2018 13:36:51 +0000 (13:36 +0000)
committerNick Clemens <nick@bywatersolutions.com>
Thu, 7 Mar 2019 17:22:43 +0000 (17:22 +0000)
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>

C4/Circulation.pm
Koha/ItemType.pm
admin/itemtypes.pl
catalogue/moredetail.pl
koha-tmpl/intranet-tmpl/prog/en/modules/admin/itemtypes.tt
koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt
t/db_dependent/Circulation.t
t/db_dependent/Koha/ItemTypes.t

index 5d2d3cd..783ba14 100644 (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
 
index 3d769a5..5faf499 100644 (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
index d397fee..723dfe9 100755 (executable)
@@ -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; };
index 6adcca9..e8736ff 100755 (executable)
@@ -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->{$_} : '' );
 }
index fdf2ab7..6c47f91 100644 (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>
index 2e10ef9..cd83edc 100644 (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 %]
index 1312a60..d9b6338 100755 (executable)
@@ -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();
+
+};
index eb08c65..34ab094 100755 (executable)
 
 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;