Support forgive-vs void on Lost/Lost-checkin
authorJeff Godin <jgodin@tadl.org>
Tue, 6 Dec 2011 18:27:09 +0000 (13:27 -0500)
committerJeff Godin <jgodin@tadl.org>
Thu, 26 Jul 2012 05:29:31 +0000 (01:29 -0400)
Constants and strings for new OU settings:

circ.forgive_overdue_on_lost
circ.forgive_lost_on_checkin
circ.forgive_lost_proc_fee_on_checkin

These are intended for use as an alternative to their "void"
counterparts in situations where the library does not refund
fees and fines, and wishes funds taken in for one purpose or
billing type to not be re-applied toward a different billing
type as a result of voiding bills after they have been paid.

When circ.forgive_overdue_on_lost is set, attempt to make a
payment of type "Forgive" on the transaction for the amount
of outstanding overdue billings.

Only outstanding bills of type 1 (Overdue materials) will
be paid, and only the first contiguous grouping. If the
system finds an outstanding billing of type other than 1,
it will pay what it has found up to that point.

circ.forgive_overdue_on_lost has priority over the "void"
version of the same setting, if both happen to be set.

When circ.forgive_lost_on_checkin and/or
circ.forgive_lost_proc_fee_on_checkin is set, make a Forgive payment
on outstanding Lost and Lost Processing Fee bills on checkin of a
lost item.

New utility function:

outstanding_bills_for_circ (in AppUtils) accepts an editor and a
circ object, and will return a reference to an array of outstanding
billing objects. this has been broken out of the forgive_overdues
sub, as it will be useful elsewhere.

Signed-off-by: Jeff Godin <jgodin@tadl.org>
Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
Open-ILS/src/perlmods/lib/OpenILS/Const.pm
Open-ILS/web/opac/locale/en-US/lang.dtd

index 792f53b..5d8668f 100644 (file)
@@ -2128,5 +2128,57 @@ sub basic_opac_copy_query {
     };
 }
 
+# -----------------------------------------------------------------
+# Given an editor and a circ, return a reference to an array of
+# billing objects which are outstanding (unpaid, not voided).
+# If a bill is partially paid, change the amount of the bill
+# to reflect the unpaid amount, not the original amount.
+# -----------------------------------------------------------------
+sub outstanding_bills_for_circ {
+    my ($self, $e, $circ) = @_;
+
+    # find all unvoided bills in order
+    my $bill_search = [
+        { xact => $circ->id, voided=>'f' },
+        { order_by => { mb => { billing_ts => { direction => 'asc' } } } },
+    ];
+
+    # find all unvoided payments in order
+    my $payment_search = [
+        { xact => $circ->id, voided=>'f' },
+        { order_by => { mp => { payment_ts => { direction => 'asc' } } } },
+    ];
+
+    my $bills = $e->search_money_billing($bill_search);
+
+    my $payments = $e->search_money_payment($payment_search);
+
+    # "Pay" the bills, removing fully paid bills and
+    # adjusting the amount for partially paid bills
+    map {
+            my $payment = $_;
+            my $paybal = $payment->amount * 100;
+
+            while ($paybal > 0) {
+                    # get next billing
+                    my $bill = shift @{$bills};
+                    my $newbal = $paybal - $bill->amount*100;
+                    if ($newbal < 0) {
+                            $newbal = 0;
+                            my $new_bill_amount = $bill->amount*100 - $paybal;
+                            $bill->amount($new_bill_amount/100);
+                            unshift(@{$bills}, $bill); # put the partially-paid bill back on top of the stack
+                    }
+
+                    $paybal = $newbal;
+
+            }
+
+    } @$payments;
+
+    return $bills;
+
+}
+
 1;
 
index ff75099..4be2774 100644 (file)
@@ -651,6 +651,8 @@ sub set_item_lost {
     # fetch the related org settings
     my $proc_fee = $U->ou_ancestor_setting_value(
         $owning_lib, OILS_SETTING_LOST_PROCESSING_FEE, $e) || 0;
+    my $forgive_overdue = $U->ou_ancestor_setting_value(
+        $owning_lib, OILS_SETTING_FORGIVE_OVERDUE_ON_LOST, $e) || 0;
     my $void_overdue = $U->ou_ancestor_setting_value(
         $owning_lib, OILS_SETTING_VOID_OVERDUE_ON_LOST, $e) || 0;
 
@@ -684,8 +686,12 @@ sub set_item_lost {
     $e->update_action_circulation($circ) or return $e->die_event;
 
     # ---------------------------------------------------------------------
-    # void all overdue fines on this circ if configured
-    if( $void_overdue ) {
+    # forgive outstanding overdue fines or void all overdue fines on this circ if configured
+    if( $forgive_overdue ) {
+        my $evt = OpenILS::Application::Circ::CircCommon->forgive_overdues($e, $circ, "System: OVERDUES FORGIVEN ON LOST");
+        return $evt if $evt;
+
+    } elsif( $void_overdue ) {
         my $evt = OpenILS::Application::Circ::CircCommon->void_overdues($e, $circ);
         return $evt if $evt;
     }
index 613447f..e2b805d 100644 (file)
@@ -17,6 +17,63 @@ my $U = "OpenILS::Application::AppUtils";
 
 
 # -----------------------------------------------------------------
+# Forgive (don't void) unpaid overdue fines on the given circ.
+# This is different from void_overdues in a few ways:
+#   * only deals with 'unpaid' overdue billings
+#   * does not accept a backdate argument
+#   * only forgives if the first unpaid billing is of type 1,
+#     and stops when it gets to a billing type other than 1
+# -----------------------------------------------------------------
+sub forgive_overdues {
+    my ($class, $e, $circ, $note) = @_;
+
+
+    $logger->info("attempting to forgive overdues on circ " . $circ->id . " with note " . $note);
+
+    # get outstanding bills for the circ in question
+    my $bills = $U->outstanding_bills_for_circ($e, $circ);
+    # Sum any outstanding overdue billings, stopping at the first non-overdue billing
+
+    my $outstanding_overdues = 0;
+
+    foreach (@$bills) {
+        my $bill = $_;
+        if ($bill->btype == 1) {
+            $logger->debug("forgive_overdues found a btype 1 bill id " . $bill->id . " amount " . $bill->amount);
+            $outstanding_overdues = ($outstanding_overdues*100 + $bill->amount*100)/100;
+        } else {
+            # We found a billing type other than 1 -- Overdue Fines
+            $logger->info("forgive_overdues found a bill id " . $bill->id . " with btype " . $bill->btype);
+            last; # stop looking for bills to forgive
+        }
+
+    }
+
+    $logger->debug("forgive_overdues outstanding balance to forgive is: " . $outstanding_overdues);
+    my $amount = $outstanding_overdues;
+
+    if ($amount >= 0.01) {
+        # pay with forgive payment
+        my $payobj = Fieldmapper::money::forgive_payment->new;
+        $payobj->amount($amount);
+        $payobj->amount_collected($amount);
+        $payobj->xact($circ->id);
+        $payobj->note($note);
+        # do we need an accepting user? who should be the accepting user?
+        $payobj->accepting_usr($e->requestor->id); # or 1?
+
+        $logger->info("forgive_overdues about to create the forgive payment... ");
+        $e->create_money_forgive_payment($payobj) or return $e->die_event;
+
+        return undef;
+    } else {
+        $logger->info("forgive_overdues found no outstanding overdues, or found outstanding billings of another type first. No forgive payment made.");
+        return undef;
+    }
+}
+
+# -----------------------------------------------------------------
 # Voids overdue fines on the given circ.  if a backdate is 
 # provided, then we only void back to the backdate, unless the
 # backdate is to within the grace period, in which case we void all
index 5bdb1c3..ea7dfdf 100644 (file)
@@ -3348,7 +3348,7 @@ sub checkin_handle_circ {
 
 
 # ------------------------------------------------------------------
-# See if we need to void billings for lost checkin
+# See if we need to void or forgive billings for lost checkin
 # ------------------------------------------------------------------
 sub checkin_handle_lost {
     my $self = shift;
@@ -3373,6 +3373,10 @@ sub checkin_handle_lost {
 
     if (!$max_return){  # there's either no max time to accept returns defined or we're within that time
 
+        my $forgive_lost = $U->ou_ancestor_setting_value(
+            $circ_lib, OILS_SETTING_FORGIVE_LOST_ON_CHECKIN, $self->editor) || 0;
+        my $forgive_lost_fee = $U->ou_ancestor_setting_value(
+            $circ_lib, OILS_SETTING_FORGIVE_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
         my $void_lost = $U->ou_ancestor_setting_value(
             $circ_lib, OILS_SETTING_VOID_LOST_ON_CHECKIN, $self->editor) || 0;
         my $void_lost_fee = $U->ou_ancestor_setting_value(
@@ -3382,8 +3386,8 @@ sub checkin_handle_lost {
         $self->generate_lost_overdue(1) if $U->ou_ancestor_setting_value(
             $circ_lib, OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN, $self->editor);
 
-        $self->checkin_handle_lost_now_found(3) if $void_lost;
-        $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
+        $self->checkin_handle_lost_now_found(3, $forgive_lost) if $forgive_lost || $void_lost;
+        $self->checkin_handle_lost_now_found(4, $forgive_lost_fee) if $forgive_lost_fee || $void_lost_fee;
         $self->checkin_handle_lost_now_found_restore_od($circ_lib) if $restore_od && ! $self->void_overdues;
     }
 
@@ -3733,31 +3737,67 @@ sub make_trigger_events {
 
 
 sub checkin_handle_lost_now_found {
-    my ($self, $bill_type) = @_;
+    my ($self, $bill_type, $forgive) = @_;
 
     # ------------------------------------------------------------------
     # remove charge from patron's account if lost item is returned
     # ------------------------------------------------------------------
 
-    my $bills = $self->editor->search_money_billing(
-        {
-            xact => $self->circ->id,
-            btype => $bill_type
-        }
-    );
+    if ($forgive) {
+        my $bills = $U->outstanding_bills_for_circ($self->editor, $self->circ);
 
-    $logger->debug("voiding lost item charge of  ".scalar(@$bills));
-    for my $bill (@$bills) {
-        if( !$U->is_true($bill->voided) ) {
-            $logger->info("lost item returned - voiding bill ".$bill->id);
-            $bill->voided('t');
-            $bill->void_time('now');
-            $bill->voider($self->editor->requestor->id);
-            my $note = ($bill->note) ? $bill->note . "\n" : '';
-            $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
+        for my $bill (@$bills) {
+            $logger->info("checkin_handle_lost_now_found found bill " . $bill->id . " type " . $bill->btype . " amount " . $bill->amount);
+        }
 
+        $logger->debug("forgiving lost item charge of  ".scalar(@$bills));
+        my $total_to_forgive = 0;
+        for my $bill (@$bills) {
+            if ($bill->btype == $bill_type) {
+                $logger->info("lost item returned - will make payment to forgive bill ".$bill->id);
+                # add this bill's amount to the total to forgive
+                $total_to_forgive = (($total_to_forgive*100) + ($bill->amount*100)) / 100;
+            } else {
+                last;
+            }
+        }
+        if ($total_to_forgive >= 0.01) {
+            # pay with forgive payment
+            my $payobj = Fieldmapper::money::forgive_payment->new;
+            $payobj->amount($total_to_forgive);
+            $payobj->amount_collected($total_to_forgive);
+            $payobj->xact($self->circ->id);
+            $payobj->note("System: FORGIVEN FOR LOST ITEM RETURNED");
+            $payobj->accepting_usr($self->editor->requestor->id);
+
+            $logger->info("checkin_handle_lost_now_found about to create forgive payment... ");
             $self->bail_on_events($self->editor->event)
-                unless $self->editor->update_money_billing($bill);
+                unless $self->editor->create_money_forgive_payment($payobj);
+        } else {
+            $logger->info("checkin_handle_lost_now_found found no billings of type $bill_type to forgive.");
+        }
+
+    } else {
+        my $bills = $self->editor->search_money_billing(
+            {
+                xact => $self->circ->id,
+                btype => $bill_type
+            }
+        );
+
+        $logger->debug("voiding lost item charge of  ".scalar(@$bills));
+        for my $bill (@$bills) {
+            if( !$U->is_true($bill->voided) ) {
+                $logger->info("lost item returned - voiding bill ".$bill->id);
+                $bill->voided('t');
+                $bill->void_time('now');
+                $bill->voider($self->editor->requestor->id);
+                my $note = ($bill->note) ? $bill->note . "\n" : '';
+                $bill->note("${note}System: VOIDED FOR LOST ITEM RETURNED");
+
+                $self->bail_on_events($self->editor->event)
+                    unless $self->editor->update_money_billing($bill);
+            }
         }
     }
 }
index f86a801..25479e2 100644 (file)
@@ -78,6 +78,7 @@ econst OILS_SETTING_LOST_PROCESSING_FEE => 'circ.lost_materials_processing_fee';
 econst OILS_SETTING_DEF_ITEM_PRICE => 'cat.default_item_price';
 econst OILS_SETTING_ORG_BOUNCED_EMAIL => 'org.bounced_emails';
 econst OILS_SETTING_CHARGE_LOST_ON_ZERO => 'circ.charge_lost_on_zero';
+econst OILS_SETTING_FORGIVE_OVERDUE_ON_LOST => 'circ.forgive_overdue_on_lost';
 econst OILS_SETTING_VOID_OVERDUE_ON_LOST => 'circ.void_overdue_on_lost';
 econst OILS_SETTING_HOLD_SOFT_STALL => 'circ.hold_stalling.soft';
 econst OILS_SETTING_HOLD_HARD_STALL => 'circ.hold_stalling.hard';
@@ -85,8 +86,10 @@ econst OILS_SETTING_HOLD_SOFT_BOUNDARY => 'circ.hold_boundary.soft';
 econst OILS_SETTING_HOLD_HARD_BOUNDARY => 'circ.hold_boundary.hard';
 econst OILS_SETTING_HOLD_EXPIRE => 'circ.hold_expire_interval';
 econst OILS_SETTING_HOLD_ESIMATE_WAIT_INTERVAL => 'circ.holds.default_estimated_wait_interval';
+econst OILS_SETTING_FORGIVE_LOST_ON_CHECKIN             => 'circ.forgive_lost_on_checkin';
 econst OILS_SETTING_VOID_LOST_ON_CHECKIN                => 'circ.void_lost_on_checkin';
 econst OILS_SETTING_MAX_ACCEPT_RETURN_OF_LOST           => 'circ.max_accept_return_of_lost';
+econst OILS_SETTING_FORGIVE_LOST_PROCESS_FEE_ON_CHECKIN => 'circ.forgive_lost_proc_fee_on_checkin';
 econst OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN    => 'circ.void_lost_proc_fee_on_checkin';
 econst OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN      => 'circ.restore_overdue_on_lost_return';
 econst OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE          => 'circ.lost_immediately_available';
index fd3e752..c6facac 100644 (file)
 <!ENTITY staff.server.admin.org_settings.global.credit.allow "Allow Credit Card Payments">
 <!ENTITY staff.server.admin.org_settings.global.credit.allow.desc "If enabled, patrons will be able to pay fines accrued at this location via credit card">
 <!ENTITY staff.server.admin.org_settings.global.default_locale "Default Locale">
+<!ENTITY staff.server.admin.org_settings.circ.forgive_overdue_on_lost "Forgive overdue fines when items are marked lost">
+<!ENTITY staff.server.admin.org_settings.circ.forgive_overdue_on_lost.desc "Forgive overdue fines when items are marked lost -- makes a Forgive payment">
 <!ENTITY staff.server.admin.org_settings.circ.void_overdue_on_lost "Void overdue fines when items are marked lost">
 <!ENTITY staff.server.admin.org_settings.circ.hold_stalling.soft 'Holds: Soft stalling interval'>
 <!ENTITY staff.server.admin.org_settings.circ.hold_stalling.soft.desc 'How long to wait before allowing remote items to be opportunisticaly captured for a hold.  Example "5 days"'>
 <!ENTITY staff.server.admin.org_settings.circ.void_item_billing_on_lost_return_before_interval "Void lost item fine when returned before interval">
 <!ENTITY staff.server.admin.org_settings.circ.void_item_billing_on_lost_return_before_interval.desc "Void lost item fine when returned before interval">
 
+<!ENTITY staff.server.admin.org_settings.circ.forgive_lost_on_checkin "Circ: Forgive lost item billing when returned">
+<!ENTITY staff.server.admin.org_settings.circ.forgive_lost_on_checkin.desc "Forgive lost item billing when returned -- makes a Forgive payment">
 <!ENTITY staff.server.admin.org_settings.circ.void_lost_on_checkin "Circ: Void lost item billing when returned">
 <!ENTITY staff.server.admin.org_settings.circ.void_lost_on_checkin.desc "Void lost item billing when returned">
 <!ENTITY staff.server.admin.org_settings.circ.max_accept_return_of_lost "Circ: Void lost max interval">
 <!ENTITY staff.server.admin.org_settings.circ.max_accept_return_of_lost.desc "Items that have been lost this long will not result in voided billings when returned.  E.g. \'6 months\'">
+<!ENTITY staff.server.admin.org_settings.circ.forgive_lost_proc_fee_on_checkin "Circ: Forgive processing fee on lost item return">
+<!ENTITY staff.server.admin.org_settings.circ.forgive_lost_proc_fee_on_checkin.desc "Forgive processing fee when lost item returned -- makes a Forgive payment">
 <!ENTITY staff.server.admin.org_settings.circ.void_lost_proc_fee_on_checkin "Circ: Void processing fee on lost item return">
 <!ENTITY staff.server.admin.org_settings.circ.void_lost_proc_fee_on_checkin.desc "Void processing fee when lost item returned">
 <!ENTITY staff.server.admin.org_settings.circ.restore_overdue_on_lost_return "Circ: Restore overdues on lost item return">