LP 1198465: Add code for adjustment payments
authorJason Stephenson <jason@sigio.com>
Fri, 20 Feb 2015 22:17:46 +0000 (17:17 -0500)
committerBen Shum <bshum@biblio.org>
Tue, 28 Jul 2015 20:24:13 +0000 (16:24 -0400)
This new payment type complements the current void logic that flags
bills as voided.

This new payment type is needed because the current way that Evergreen
voids bills requires that all voids happen in the same increment as the
bills themselves.  This prevents voiding of a partial bill or a bill
that has had a partial payment applied.

This commit also adds the org. unit setting types for the conditional
negative balances enhancements:

bill.prohibit_negative_balance_default
bill.prohibit_negative_balance_on_overdues
bill.prohibit_negative_balance_on_lost
bill.negative_balance_interval_default
bill.negative_balance_interval_on_overdues
bill.negative_balance_interval_on_lost

Finally, create a helper function for checking intervals along the way,
and a handy little subroutine to void all bills of a given type on a
circulation transaction.

Signed-off-by: Kathy Lussier <klussier@masslnc.org>
Conflicts:
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm
Open-ILS/src/sql/Pg/950.data.seed-values.sql

Signed-off-by: Dan Wells <dbw2@calvin.edu>
Signed-off-by: Kathy Lussier <klussier@masslnc.org>
Signed-off-by: Ben Shum <bshum@biblio.org>
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm
Open-ILS/src/sql/Pg/080.schema.money.sql
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.data.conditional_negative_balance.sql [new file with mode: 0644]

index b847d16..cd9d697 100644 (file)
@@ -81,6 +81,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field name="work_payment" oils_persist:virtual="true" />
                        <field name="credit_payment" oils_persist:virtual="true" />
                        <field name="goods_payment" oils_persist:virtual="true" />
+                       <field name="adjustment_payment" oils_persist:virtual="true" />
                </fields>
                <links>
                        <link field="usr" reltype="has_a" key="id" map="" class="au"/>
@@ -3621,6 +3622,26 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        </actions>
                </permacrud>
        </class>
+       <class id="map" controller="open-ils.cstore" oils_obj:fieldmapper="money::adjustment_payment" oils_persist:tablename="money.adjustment_payment" reporter:label="Adjustment Payment">
+               <fields oils_persist:primary="id" oils_persist:sequence="money.payment_id_seq">
+                       <field name="accepting_usr" reporter:datatype="link"/>
+                       <field name="amount" reporter:datatype="money" />
+                       <field name="amount_collected" reporter:datatype="money" />
+                       <field name="id" reporter:datatype="id" />
+                       <field name="note"  reporter:datatype="text"/>
+                       <field name="payment_ts" reporter:datatype="timestamp"/>
+                       <field name="xact" reporter:datatype="link"/>
+                       <field name="billing" reporter:datatype="link"/>
+                       <field name="payment_type" oils_persist:virtual="true"  reporter:datatype="text"/>
+                       <field name="payment" oils_persist:virtual="true" reporter:datatype="link"/>
+               </fields>
+               <links>
+                       <link field="payment" reltype="might_have" key="id" map="" class="mp"/>
+                       <link field="accepting_usr" reltype="has_a" key="id" map="" class="au"/>
+                       <link field="xact" reltype="has_a" key="id" map="" class="mbt"/>
+                       <link field="billing" reltype="might_have" key="id" class="mb"/>
+               </links>
+       </class>
        <class id="mrd" controller="open-ils.cstore" oils_obj:fieldmapper="metabib::record_descriptor" oils_persist:tablename="metabib.rec_descriptor" reporter:label="Basic Record Descriptor">
                <fields oils_persist:primary="id" oils_persist:sequence="metabib.rec_descriptor_id_seq">
                        <field reporter:label="Audn" name="audience" oils_persist:primitive="string"  reporter:datatype="text"/>
@@ -6928,6 +6949,7 @@ SELECT  usr,
                        <field reporter:label="Work Payment Detail" name="work_payment" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="Forgive Payment Detail" name="forgive_payment" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="Goods Payment Detail" name="goods_payment" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Adjustment Payment Detail" name="adjustment_payment" oils_persist:virtual="true" reporter:datatype="link"/>
                </fields>
                <links>
                        <link field="cash_payment" reltype="might_have" key="id" map="" class="mcp"/>
@@ -6937,6 +6959,7 @@ SELECT  usr,
                        <link field="work_payment" reltype="might_have" key="id" map="" class="mwp"/>
                        <link field="forgive_payment" reltype="might_have" key="id" map="" class="mfp"/>
                        <link field="goods_payment" reltype="might_have" key="id" map="" class="mgp"/>
+                       <link field="adjustment_payment" reltype="might_have" key="id" map="" class="map"/>
                        <link field="xact" reltype="has_a" key="id" map="" class="mbt"/>
                </links>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
@@ -6964,6 +6987,7 @@ SELECT  usr,
                        <field reporter:label="Work Payment Detail" name="work_payment" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="Forgive Payment Detail" name="forgive_payment" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="Goods Payment Detail" name="goods_payment" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Adjustment Payment Detail" name="adjustment_payment" oils_persist:virtual="true" reporter:datatype="link"/>
                </fields>
                <links>
                        <link field="cash_payment" reltype="might_have" key="id" map="" class="mcp"/>
@@ -6973,6 +6997,7 @@ SELECT  usr,
                        <link field="work_payment" reltype="might_have" key="id" map="" class="mwp"/>
                        <link field="forgive_payment" reltype="might_have" key="id" map="" class="mfp"/>
                        <link field="goods_payment" reltype="might_have" key="id" map="" class="mgp"/>
+                       <link field="adjustment_payment" reltype="might_have" key="id" map="" class="mvp"/>
                        <link field="xact" reltype="has_a" key="id" map="" class="mbt"/>
                        <link field="accepting_usr" reltype="has_a" key="id" map="" class="au"/>
                </links>
@@ -6990,12 +7015,14 @@ SELECT  usr,
                        <field reporter:label="Forgive Payment Detail" name="forgive_payment" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="Goods Payment Detail" name="goods_payment" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="Credit Payment Detail" name="credit_payment" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Adjustment Payment Detail" name="adjustment_payment" oils_persist:virtual="true" reporter:datatype="link"/>
                </fields>
                <links>
                        <link field="work_payment" reltype="might_have" key="id" map="" class="mwp"/>
                        <link field="forgive_payment" reltype="might_have" key="id" map="" class="mfp"/>
                        <link field="goods_payment" reltype="might_have" key="id" map="" class="mgp"/>
                        <link field="credit_payment" reltype="might_have" key="id" map="" class="mcrp"/>
+                       <link field="adjustment_payment" reltype="might_have" key="id" map="" class="mvp"/>
                        <link field="xact" reltype="has_a" key="id" map="" class="mbt"/>
                </links>
        </class>
index 2a4e184..8c92ee5 100644 (file)
@@ -2219,5 +2219,26 @@ sub check_open_xact {
     return undef;
 }
 
+# Because floating point math has rounding issues, and Dyrcona gets
+# tired of typing out the code to multiply floating point numbers
+# before adding and subtracting them and then dividing the result by
+# 100 each time, he wrote this little subroutine for subtracting
+# floating point values.  It can serve as a model for the other
+# operations if you like.
+#
+# It takes a list of floating point values as arguments.  The rest are
+# all subtracted from the first and the result is returned.  The
+# values are all multiplied by 100 before being used, and the result
+# is divided by 100 in order to avoid decimal rounding errors inherent
+# in floating point math.
+sub fpdiff {
+    my ($class, @args) = @_;
+    my $result = shift(@args) * 100;
+    while (my $arg = shift(@args)) {
+        $result -= $arg * 100;
+    }
+    return $result / 100;
+}
+
 1;
 
index 19b2787..c27df39 100644 (file)
@@ -9,6 +9,7 @@ use OpenSRF::Utils::Logger qw(:logger);
 use OpenILS::Utils::CStoreEditor q/:funcs/;
 use OpenILS::Const qw/:const/;
 use POSIX qw(ceil);
+use List::MoreUtils qw(uniq);
 
 my $U = "OpenILS::Application::AppUtils";
 my $parser = DateTime::Format::ISO8601->new;
@@ -58,17 +59,12 @@ sub void_overdues {
         }
     }
 
-    my $bills = $e->search_money_billing($bill_search);
-    
-    for my $bill (@$bills) {
-        next if $U->is_true($bill->voided);
-        $logger->info("voiding overdue bill ".$bill->id);
-        $bill->voided('t');
-        $bill->void_time('now');
-        $bill->voider($e->requestor->id);
-        my $n = ($bill->note) ? sprintf("%s\n", $bill->note) : "";
-        $bill->note(sprintf("$n%s", ($note) ? $note : "System: VOIDED FOR BACKDATE"));
-        $e->update_money_billing($bill) or return $e->die_event;
+    my $billids = $e->search_money_billing([$bill_search, {idlist=>1}]);
+    if ($billids && @$billids) {
+        my $result = $class->real_void_bills($e, $billids, $note);
+        if (ref($result)) {
+            return $result;
+        }
     }
 
     return undef;
@@ -105,6 +101,33 @@ sub void_lost {
     return undef;
 }
 
+# ------------------------------------------------------------------
+# Void all bills of a given type on a circulation.
+#
+# Takes an editor, a circ object, the btype number for the bills you
+# want to void, and an optional note.
+#
+# Returns undef on success or the result from real_void_bills.
+# ------------------------------------------------------------------
+sub void_bills_of_type {
+    my ($class, $e, $circ, $btype, $note) = @_;
+
+    # Get a bill payment map.
+    my $bpmap = $class->bill_payment_map_for_xact($e, $circ);
+    if ($bpmap && @$bpmap) {
+        # Filter out the unvoided bills of the type we're looking for:
+        my @bills = map {$_->{bill}} grep { $_->{bill}->btype() == $btype && $_->{bill_amount} > $_->{void_amount} } @$bpmap;
+        if (@bills) {
+            my $result = $class->real_void_bills($e, \@bills, $note);
+            if (ref($result)) {
+                return $result;
+            }
+        }
+    }
+
+    return undef;
+}
+
 sub reopen_xact {
     my($class, $e, $xactid) = @_;
 
@@ -591,4 +614,404 @@ sub generate_fines {
     return undef;
 }
 
+# -----------------------------------------------------------------
+# Given an editor and a xact, return a reference to an array of
+# hashrefs that map billing objects to payment objects.  Returns undef
+# if no bills are found for the given transaction.
+#
+# The bill amounts are adjusted to reflect the application of the
+# payments to the bills.  The original bill amounts are retained in
+# the mapping.
+#
+# The payment objects may or may not have their amounts adjusted
+# depending on whether or not they apply to more than one bill.  We
+# could really use a better logic here, perhaps, but if it was
+# consistent, it wouldn't be Evergreen.
+#
+# The data structure used in the array is a hashref that has the
+# following fields:
+#
+# bill => the adjusted bill object
+# voids => an arrayref of void payments that apply directly to the
+#          bill
+# payments => an arrayref of payment objects applied to the bill
+# bill_amount => original amount from the billing object
+# void_amount => total of the void payments that apply directly to the
+#                bill
+#
+# Each bill is only mapped to payments one time.  However, a single
+# payment may be mapped to more than one bill if the payment amount is
+# greater than the amount of each individual bill, such as a $3.00
+# payment for 30 $0.10 overdue bills.  There is an attempt made to
+# first pay bills with payments that match the billing amount.  This
+# is intended to catch payments for lost and/or long overdue bills so
+# that they will match up.
+#
+# This function is heavily adapted from code written by Jeff Godin of
+# Traverse Area District Library and submitted on LaunchPad bug
+# #1009049.
+# -----------------------------------------------------------------
+sub bill_payment_map_for_xact {
+    my ($class, $e, $xact) = @_;
+
+    # Check for CStoreEditor and make a new one if we have to. This
+    # allows one-off calls to this subroutine to pass undef as the
+    # CStoreEditor and not have to create one of their own.
+    $e = OpenILS::Utils::CStoreEditor->new unless ($e);
+
+    # find all bills in order
+    my $bill_search = [
+        {xact => $xact->id()},
+        { order_by => { mb => { billing_ts => { direction => 'asc' } } } },
+    ];
+
+    # At some point, we should get rid of the voided column on
+    # money.payment and family.  It is not exposed in the client at
+    # the moment, and should be replaced with a void_bill type.  The
+    # descendants of money.payment don't expose the voided field in
+    # the fieldmapper, only the mp object, based on the money.payment
+    # view, does.  However, I want to leave that complication for
+    # later.  I wonder if I'm not slowing things down too much with
+    # the current adjustment_payment logic.  It would probably be faster if
+    # we had direct Pg access at this layer.  I can probably wrangle
+    # something via the drivers or store interfaces, but I haven't
+    # really figured those out, yet.
+
+    my $bills = $e->search_money_billing($bill_search);
+
+    # return undef if there are no bills.
+    return undef unless ($bills && @$bills);
+
+    # map the bills into our bill_payment_map entry format:
+    my @entries = map {
+        {
+            bill => $_,
+            bill_amount => $_->amount(),
+            payments => [],
+            voids => [],
+            void_amount => 0
+        }
+    } @$bills;
+
+    # Find all unvoided payments in order.  Flesh voids so that we
+    # don't have to retrieve them later.
+    my $payments = $e->search_money_payment(
+        [
+            { xact => $xact->id, voided=>'f' },
+            {
+                order_by => { mp => { payment_ts => { direction => 'asc' } } },
+                flesh => 1,
+                flesh_fields => { mp => ['adjustment_payment'] }
+            }
+        ]
+    );
+
+    # If there were no payments, then we just return the bills.
+    return \@entries unless ($payments && @$payments);
+
+    # Now, we go through the rigmarole of mapping payments to bills
+    # and adjusting the bill balances.
+
+    # Apply the voids before "paying" other bills.
+    foreach my $entry (@entries) {
+        my $bill = $entry->{bill};
+        # Find only the voids that apply to individual bills.
+        my @voids = map {$_->adjustment_payment()} grep {$_->payment_type() eq 'adjustment_payment' && $_->adjustment_payment()->billing() == $bill->id()} @$payments;
+        if (@voids) {
+            foreach my $void (@voids) {
+                my $new_amount = $U->fpdiff($bill->amount(),$void->amount());
+                if ($new_amount >= 0) {
+                    push @{$entry->{voids}}, $void;
+                    $entry->{void_amount} += $void->amount();
+                    $bill->amount($new_amount);
+                    # Remove the used up void from list of payments:
+                    my @p = grep {$_->id() != $void->id()} @$payments;
+                    $payments = \@p;
+                } else {
+                    # It should never happen that we have more void
+                    # payments on a single bill than the amount of the
+                    # bill.  However, experience shows that the things
+                    # that should never happen actually do happen with
+                    # surprising regularity in a library setting.
+
+                    # Clone the void to say how much of it actually
+                    # applied to this bill.
+                    my $new_void = $void->clone();
+                    $new_void->amount($bill->amount());
+                    $new_void->amount_collected($bill->amount());
+                    push (@{$entry->{voids}}, $new_void);
+                    $entry->{void_amount} += $new_void->amount();
+                    $bill->amount(0);
+                    $void->amount(-$new_amount);
+                    # Could be a candidate for YAOUS about what to do
+                    # with excess void amounts on a bill.
+                }
+                last if ($bill->amount() == 0);
+            }
+        }
+    }
+
+    # Try to map payments to bills by amounts starting with the
+    # largest payments:
+    foreach my $payment (sort {$b->amount() <=> $a->amount()} @$payments) {
+        my @bills2pay = grep {$_->{bill}->amount() == $payment->amount()} @entries;
+        if (@bills2pay) {
+            my $entry = $bills2pay[0];
+            $entry->{bill}->amount(0);
+            push @{$entry->{payments}}, $payment;
+            # Remove the payment from the master list.
+            my @p = grep {$_->id() != $payment->id()} @$payments;
+            $payments = \@p;
+        }
+    }
+
+    # Map remaining bills to payments in whatever order.
+    foreach  my $entry (grep {$_->{bill}->amount() > 0} @entries) {
+        my $bill = $entry->{bill};
+        # We could run out of payments before bills.
+        if ($payments && @$payments) {
+            while ($bill->amount() > 0) {
+                my $payment = shift @$payments;
+                last unless $payment;
+                my $new_amount = $U->fpdiff($bill->amount(),$payment->amount());
+                if ($new_amount < 0) {
+                    # Clone the payment so we can say how much applied
+                    # to this bill.
+                    my $new_payment = $payment->clone();
+                    $new_payment->amount($bill->amount());
+                    $bill->amount(0);
+                    push @{$entry->{payments}}, $new_payment;
+                    # Reset the payment amount and put it back on the
+                    # list for later use.
+                    $payment->amount(-$new_amount);
+                    unshift @$payments, $payment;
+                } else {
+                    $bill->amount($new_amount);
+                    push @{$entry->{payments}}, $payment;
+                }
+            }
+        }
+    }
+
+    return \@entries;
+}
+
+
+# This subroutine actually handles voiding of bills.  It takes a
+# CStoreEditor, an arrayref of bill ids or bills, and an optional note.
+sub real_void_bills {
+    my ($class, $e, $billids, $note) = @_;
+
+    # Get with the editor to see if we have permission to void bills.
+    return $e->die_event unless $e->checkauth;
+    return $e->die_event unless $e->allowed('VOID_BILLING');
+
+    my %users;
+
+    # Let's get all the billing objects and handle them by
+    # transaction.
+    my $bills;
+    if (ref($billids->[0])) {
+        $bills = $billids;
+    } else {
+        $bills = $e->search_money_billing([{id => $billids}])
+            or return $e->die_event;
+    }
+
+    my @xactids = uniq map {$_->xact()} @$bills;
+
+    foreach my $xactid (@xactids) {
+        my $mbt = $e->retrieve_money_billable_transaction(
+            [
+                $xactid,
+                {
+                    flesh=> 2,
+                    flesh_fields=> {
+                        mbt=>['grocery','circulation'],
+                        circ=>['target_copy']
+                    }
+                }
+            ]
+        ) or return $e->die_event;
+        # Flesh grocery bills and circulations so we don't have to
+        # retrieve them later.
+        my ($circ, $grocery, $copy);
+        $grocery = $mbt->grocery();
+        $circ = $mbt->circulation();
+        $copy = $circ->target_copy() if ($circ);
+
+        # Retrieve settings based on transaction location and copy
+        # location if we have a circulation.
+        my ($prohibit_neg_balance_default, $prohibit_neg_balance_overdues,
+            $prohibit_neg_balance_lost, $neg_balance_interval_default,
+            $neg_balance_interval_overdues, $neg_balance_interval_lost);
+        if ($circ) {
+            # defaults and overdue settings come from transaction org unit.
+            $prohibit_neg_balance_default = $U->ou_ancestor_setting(
+                $circ->circ_lib(), 'bill.prohibit_negative_balance_default');
+            $prohibit_neg_balance_overdues = (
+                $U->ou_ancestor_setting($circ->circ_lib(), 'bill.prohibit_negative_balance_on_overdues')
+                ||
+                $U->ou_ancestor_setting($circ->circ_lib(), 'bill.prohibit_netgative_balance_default')
+            );
+            $neg_balance_interval_default = $U->ou_ancestor_setting(
+                $circ->circ_lib(), 'bill.negative_balance_interval_default');
+            $neg_balance_interval_overdues = (
+                $U->ou_ancestor_setting($circ->circ_lib(), 'bill.negative_balance_interval_on_overdues')
+                ||
+                $U->ou_ancestor_setting($circ->circ_lib(), 'bill.negative_balance_interval_default')
+            );
+            # settings for lost come from copy circlib.
+            $prohibit_neg_balance_lost = (
+                $U->ou_ancestor_setting($copy->circ_lib(), 'bill.prohibit_negative_balance_on_lost')
+                ||
+                $U->ou_ancestor_setting($copy->circ_lib(), 'bill.prohibit_negative_balance_default')
+            );
+            $neg_balance_interval_lost = (
+                $U->ou_ancestor_setting($copy->circ_lib(), 'bill.negative_balance_interval_on_lost')
+                ||
+                $U->ou_ancestor_setting($copy->circ_lib(), 'bill.negative_balance_interval_default')
+            );
+        } else {
+            # We only care about defaults, and they come from the
+            # billing location.
+            $prohibit_neg_balance_default = $U->ou_ancestor_setting(
+                $grocery->billing_location(), 'bill.prohibit_negative_balance_default');
+            $neg_balance_interval_default = $U->ou_ancestor_setting(
+            $grocery->billing_location(), 'bill.negative_balance_interval_default');
+        }
+
+        # Get the bill_payment_map for the transaction.
+        my $bpmap = $class->bill_payment_map_for_xact($e, $mbt);
+
+        # Get the bills for this transaction from the main list of bills.
+        my @xact_bills = grep {$_->xact() == $xactid} @$bills;
+        # Handle each bill in turn.
+        foreach my $bill (@xact_bills) {
+            # As the total open amount on the transaction will change
+            # as each bill is voided, we'll just recalculate it for
+            # each bill.
+            my $xact_total = 0;
+            map {$xact_total += $_->{bill}->amount()} @$bpmap;
+
+            # Get the bill_payment_map entry for this bill:
+            my ($bpentry) = grep {$_->{bill}->id() == $bill->id()} @$bpmap;
+
+            # From here on out, use the bill object from the bill
+            # payment map entry.
+            $bill = $bpentry->{bill};
+
+            # The amount to void is the non-voided balance on the
+            # bill. It should never be less than zero.
+            my $amount_to_void = $U->fpdiff($bpentry->{bill_amount},$bpentry->{void_amount});
+
+            # Check if this bill is already voided.  We don't allow
+            # "double" voids regardless of settings.  The old code
+            # made it impossible to void an already voided bill, so
+            # we're doing the same.
+            if ($amount_to_void <= 0) {
+                my $event = OpenILS::Event->new('BILL_ALREADY_VOIDED', payload => $bill);
+                $e->event($event);
+                return $event;
+            }
+
+            # If we're voiding a circulation-related bill we have
+            # stuff to check.
+            if ($circ) {
+                if ($amount_to_void > $xact_total) {
+                    my $btype = $bill->btype();
+                    if ($btype == 1) {
+                        # Overdues
+                        $amount_to_void = $xact_total unless(_check_payment_interval($bpentry, $neg_balance_interval_overdues));
+                        $amount_to_void = $xact_total if ($U->is_true($prohibit_neg_balance_overdues));
+                    } elsif ($btype == 3 || $btype == 10) {
+                        # Lost or Long Overdue
+                        $amount_to_void = $xact_total unless(_check_payment_interval($bpentry, $neg_balance_interval_lost));
+                        $amount_to_void = $xact_total if ($U->is_true($prohibit_neg_balance_lost));
+                    } else {
+                        # Any other bill that we're trying to void.
+                        $amount_to_void = $xact_total unless(_check_payment_interval($bpentry, $neg_balance_interval_default));
+                        $amount_to_void = $xact_total if ($U->is_true($prohibit_neg_balance_default));
+                    }
+                }
+            } else {
+                # Grocery bills are simple by comparison.
+                if ($amount_to_void > $xact_total) {
+                    $amount_to_void = $xact_total unless(_check_payment_interval($bpentry, $neg_balance_interval_default));
+                    $amount_to_void = $xact_total if ($U->is_true($prohibit_neg_balance_default));
+                }
+            }
+
+            # Create the void payment if necessary:
+            if ($amount_to_void > 0) {
+                my $payobj = Fieldmapper::money::adjustment_payment->new;
+                $payobj->amount($amount_to_void);
+                $payobj->amount_collected($amount_to_void);
+                $payobj->xact($xactid);
+                $payobj->accepting_usr($e->requestor->id);
+                $payobj->payment_ts('now');
+                $payobj->billing($bill->id());
+                $payobj->note($note) if ($note);
+                $e->create_money_adjustment_payment($payobj) or return $e->die_event;
+                # Adjust our bill_payment_map
+                $bpentry->{void_amount} += $amount_to_void;
+                push @{$bpentry->{voids}}, $payobj;
+                # Should come to zero:
+                my $new_bill_amount = $U->fpdiff($bill->amount(),$amount_to_void);
+                $bill->amount($new_bill_amount);
+            }
+        }
+
+        my $org = $U->xact_org($xactid, $e);
+        $users{$mbt->usr} = {} unless $users{$mbt->usr};
+        $users{$mbt->usr}->{$org} = 1;
+
+        my $evt = $U->check_open_xact($e, $xactid, $mbt);
+        return $evt if $evt;
+    }
+
+    # calculate penalties for all user/org combinations
+    for my $user_id (keys %users) {
+        for my $org_id (keys %{$users{$user_id}}) {
+            OpenILS::Utils::Penalty->calculate_penalties($e, $user_id, $org_id);
+        }
+    }
+
+    return 1;
+}
+
+# A helper function to check if the payments on a bill are within the
+# range of a given interval.  The first argument is the entry hash
+# from the bill payment map for the bill to check and the second
+# argument is the interval.  It returns true (1) if any of the bills
+# are within range of the interval, or false (0) otherwise.  It also
+# returns true if the interval argument is undefined or empty, or if
+# the bill has no payments whatsoever.  It will return false if the
+# entry has no payments other than voids.
+sub _check_payment_interval {
+    my ($entry, $interval) = @_;
+    my $result = ($interval ? 0 : 1);
+
+    # A check to see if we were given the settings hash or the value:
+    if (ref($interval) eq 'HASH') {
+        $interval = $interval->{value};
+    }
+
+    if ($interval && $entry && $entry->{payments} && @{$entry->{payments}}) {
+        my $interval_secs = interval_to_seconds($interval);
+        my @pay_dates = map {$_->payment_ts()} sort {$b->payment_ts() cmp $a->payment_ts()}  grep {$_->payment_type() ne 'adjustment_payment'} @{$entry->{payments}};
+        if (@pay_dates) {
+            # Since we've sorted the payment dates from highest to
+            # lowest, we really only need to check the 0th one.
+            my $payment_date = DateTime::Format::ISO8601->parse_datetime(cleanse_ISO8601($pay_dates[0]))->epoch;
+            my $now = time;
+            $result = 1 if ($payment_date + $interval_secs >= $now);
+        }
+    } elsif ($interval && (!$entry->{payments} || !@{$entry->{payments}})) {
+        $result = 1;
+    }
+
+    return $result;
+}
+
 1;
index 6c59ad1..05cc461 100644 (file)
@@ -17,8 +17,10 @@ package OpenILS::Application::Circ::Money;
 use base qw/OpenILS::Application/;
 use strict; use warnings;
 use OpenILS::Application::AppUtils;
+use OpenILS::Application::Circ::CircCommon;
 my $apputils = "OpenILS::Application::AppUtils";
 my $U = "OpenILS::Application::AppUtils";
+my $CC = "OpenILS::Application::Circ::CircCommon";
 
 use OpenSRF::EX qw(:try);
 use OpenILS::Perm;
@@ -482,7 +484,7 @@ sub make_payments {
             # close if no circulation transaction is present,
             # otherwise we check if the circulation is in a state that
             # allows itself to be closed.
-            if (!$circ || OpenILS::Application::Circ::CircCommon->can_close_circ($e, $circ)) {
+            if (!$circ || $CC->can_close_circ($e, $circ)) {
                 $trans = $e->retrieve_money_billable_transaction($transid);
                 $trans->xact_finish("now");
                 if (!$e->update_money_billable_transaction($trans)) {
@@ -904,46 +906,16 @@ __PACKAGE__->register_method(
 );
 sub void_bill {
     my( $s, $c, $authtoken, @billids ) = @_;
-
-    my $e = new_editor( authtoken => $authtoken, xact => 1 );
-    return $e->die_event unless $e->checkauth;
-    return $e->die_event unless $e->allowed('VOID_BILLING');
-
-    my %users;
-    for my $billid (@billids) {
-
-        my $bill = $e->retrieve_money_billing($billid)
-            or return $e->die_event;
-
-        my $xact = $e->retrieve_money_billable_transaction($bill->xact)
-            or return $e->die_event;
-
-        if($U->is_true($bill->voided)) {
-            $e->rollback;
-            return OpenILS::Event->new('BILL_ALREADY_VOIDED', payload => $bill);
-        }
-
-        my $org = $U->xact_org($bill->xact, $e);
-        $users{$xact->usr} = {} unless $users{$xact->usr};
-        $users{$xact->usr}->{$org} = 1;
-
-        $bill->voided('t');
-        $bill->voider($e->requestor->id);
-        $bill->void_time('now');
-    
-        $e->update_money_billing($bill) or return $e->die_event;
-        my $evt = $U->check_open_xact($e, $bill->xact, $xact);
-        return $evt if $evt;
-    }
-
-    # calculate penalties for all user/org combinations
-    for my $user_id (keys %users) {
-        for my $org_id (keys %{$users{$user_id}}) {
-            OpenILS::Utils::Penalty->calculate_penalties($e, $user_id, $org_id);
-        }
+    my $editor = new_editor(authtoken=>$authtoken, xact=>1);
+    my $rv = $CC->real_void_bills($editor, \@billids);
+    if (ref($rv) eq 'HASH') {
+        # We got an event.
+        $editor->rollback();
+    } else {
+        # We should have gotten 1.
+        $editor->commit();
     }
-    $e->commit;
-    return 1;
+    return $rv;
 }
 
 
index fc0cfe3..2becfb5 100644 (file)
@@ -540,6 +540,20 @@ CREATE TRIGGER mat_summary_add_tgr AFTER INSERT ON money.forgive_payment FOR EAC
 CREATE TRIGGER mat_summary_upd_tgr AFTER UPDATE ON money.forgive_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_update ('forgive_payment');
 CREATE TRIGGER mat_summary_del_tgr BEFORE DELETE ON money.forgive_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_del ('forgive_payment');
 
+CREATE TABLE money.adjustment_payment (
+    billing BIGINT REFERENCES money.billing (id) ON DELETE SET NULL
+) INHERITS (money.bnm_payment);
+ALTER TABLE money.adjustment_payment ADD PRIMARY KEY (id);
+CREATE INDEX money_adjustment_id_idx ON money.adjustment_payment (id);
+CREATE INDEX money_adjustment_payment_xact_idx ON money.adjustment_payment (xact);
+CREATE INDEX money_adjustment_payment_bill_idx ON money.adjustment_payment (billing);
+CREATE INDEX money_adjustment_payment_payment_ts_idx ON money.adjustment_payment (payment_ts);
+CREATE INDEX money_adjustment_payment_accepting_usr_idx ON money.adjustment_payment (accepting_usr);
+
+CREATE TRIGGER mat_summary_add_tgr AFTER INSERT ON money.adjustment_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_add ('adjustment_payment');
+CREATE TRIGGER mat_summary_upd_tgr AFTER UPDATE ON money.adjustment_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_update ('adjustment_payment');
+CREATE TRIGGER mat_summary_del_tgr BEFORE DELETE ON money.adjustment_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_del ('adjustment_payment');
+
 
 CREATE TABLE money.work_payment () INHERITS (money.bnm_payment);
 ALTER TABLE money.work_payment ADD PRIMARY KEY (id);
index bac8cfc..97b9537 100644 (file)
@@ -5007,6 +5007,66 @@ INSERT into config.org_unit_setting_type
         'coust', 'description'),
     'bool', null)
 
+,(  'bill.prohibit_negative_balance_default', 'finance',
+    oils_i18n_gettext(
+        'bill.prohibit_negative_balance_default',
+        'Prohibit negative balance on bills (DEFAULT)',
+        'coust', 'label'),
+    oils_i18n_gettext(
+        'bill.prohibit_negative_balance_default',
+        'Default setting to prevent credits on circulation related bills',
+        'coust', 'description'),
+    'bool', null)
+,(  'bill.prohibit_negative_balance_on_overdues', 'finance',
+    oils_i18n_gettext(
+        'bill.prohibit_negative_balance_on_overdues',
+        'Prohibit negative balance on bills for overdue materials',
+        'coust', 'label'),
+    oils_i18n_gettext(
+        'bill.prohibit_negative_balance_on_overdues',
+        'Prevent credits on bills for overdue materials',
+        'coust', 'description'),
+    'bool', null)
+,(  'bill.prohibit_negative_balance_on_lost', 'finance',
+    oils_i18n_gettext(
+        'bill.prohibit_negative_balance_on_lost',
+        'Prohibit negative balance on bills for lost materials',
+        'coust', 'label'),
+    oils_i18n_gettext(
+        'bill.prohibit_negative_balance_on_lost',
+        'Prevent credits on bills for lost/long overdue materials',
+        'coust', 'description'),
+    'bool', null)
+,(  'bill.negative_balance_interval_default', 'finance',
+    oils_i18n_gettext(
+        'bill.negative_balance_interval_default',
+        'Negative Balance Interval (DEFAULT)',
+        'coust', 'label'),
+    oils_i18n_gettext(
+        'bill.negative_balance_interval_default',
+        'Amount of time after which no negative balances or credits are allowed on circulation bills',
+        'coust', 'description'),
+    'interval', null)
+,(  'bill.negative_balance_interval_on_overdues', 'finance',
+    oils_i18n_gettext(
+        'bill.negative_balance_interval_on_overdues',
+        'Negative Balance Interval for Overdues',
+        'coust', 'label'),
+    oils_i18n_gettext(
+        'bill.negative_balance_interval_on_overdues',
+        'Amount of time after which no negative balances or credits are allowed on bills for overdue materials',
+        'coust', 'description'),
+    'interval', null)
+,(  'bill.negative_balance_interval_on_lost', 'finance',
+    oils_i18n_gettext(
+        'bill.negative_balance_interval_on_lost',
+        'Negative Balance Interval for Lost',
+        'coust', 'label'),
+    oils_i18n_gettext(
+        'bill.negative_balance_interval_on_lost',
+        'Amount of time after which no negative balances or credits are allowed on bills for lost/long overdue materials',
+        'coust', 'description'),
+    'interval', null)
 ;
 
 UPDATE config.org_unit_setting_type
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.conditional_negative_balance.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.conditional_negative_balance.sql
new file mode 100644 (file)
index 0000000..1d03ec9
--- /dev/null
@@ -0,0 +1,90 @@
+BEGIN;
+
+-- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+CREATE TABLE money.adjustment_payment (
+    billing BIGINT REFERENCES money.billing (id) ON DELETE SET NULL
+) INHERITS (money.bnm_payment);
+ALTER TABLE money.adjustment_payment ADD PRIMARY KEY (id);
+CREATE INDEX money_adjustment_id_idx ON money.adjustment_payment (id);
+CREATE INDEX money_adjustment_payment_xact_idx ON money.adjustment_payment (xact);
+CREATE INDEX money_adjustment_payment_bill_idx ON money.adjustment_payment (billing);
+CREATE INDEX money_adjustment_payment_payment_ts_idx ON money.adjustment_payment (payment_ts);
+CREATE INDEX money_adjustment_payment_accepting_usr_idx ON money.adjustment_payment (accepting_usr);
+
+CREATE TRIGGER mat_summary_add_tgr AFTER INSERT ON money.adjustment_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_add ('adjustment_payment');
+CREATE TRIGGER mat_summary_upd_tgr AFTER UPDATE ON money.adjustment_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_update ('adjustment_payment');
+CREATE TRIGGER mat_summary_del_tgr BEFORE DELETE ON money.adjustment_payment FOR EACH ROW EXECUTE PROCEDURE money.materialized_summary_payment_del ('adjustment_payment');
+
+-- Insert new org. unit settings.
+INSERT INTO config.org_unit_setting_type 
+       (name, grp, datatype, label, description)
+VALUES
+       ('bill.prohibit_negative_balance_default',
+        'finance', 'bool',
+        oils_i18n_gettext(
+            'bill.prohibit_negative_balance_default',
+            'Prohibit negative balance on bills (DEFAULT)',
+            'coust', 'label'),
+        oils_i18n_gettext(
+            'bill.prohibit_negative_balance_default',
+            'Default setting to prevent credits on circulation related bills',
+            'coust', 'description')
+       ),
+       ('bill.prohibit_negative_balance_on_overdues',
+        'finance', 'bool',
+        oils_i18n_gettext(
+            'bill.prohibit_negative_balance_on_overdues',
+            'Prohibit negative balance on bills for overdue materials',
+            'coust', 'label'),
+        oils_i18n_gettext(
+            'bill.prohibit_negative_balance_on_overdues',
+            'Prevent credits on bills for overdue materials',
+            'coust', 'description')
+       ),
+       ('bill.prohibit_negative_balance_on_lost',
+        'finance', 'bool',
+        oils_i18n_gettext(
+            'bill.prohibit_negative_balance_on_lost',
+            'Prohibit negative balance on bills for lost materials',
+            'coust', 'label'),
+        oils_i18n_gettext(
+            'bill.prohibit_negative_balance_on_lost',
+            'Prevent credits on bills for lost/long-overde materials',
+            'coust', 'description')
+       ),
+       ('bill.negative_balance_interval_default',
+        'finance', 'interval',
+        oils_i18n_gettext(
+            'bill.negative_balance_interval_default',
+            'Negative Balance Interval (DEFAULT)',
+            'coust', 'label'),
+        oils_i18n_gettext(
+            'bill.negative_balance_interval_default',
+            'Amount of time after which no negative balances or credits are allowed on circulation bills',
+            'coust', 'description')
+       ),
+       ('bill.negative_balance_interval_on_overdues',
+        'finance', 'interval',
+        oils_i18n_gettext(
+            'bill.negative_balance_interval_on_overdues',
+            'Negative Balance Interval for Overdues',
+            'coust', 'label'),
+        oils_i18n_gettext(
+            'bill.negative_balance_interval_on_overdues',
+            'Amount of time after which no negative balances or credits are allowed on bills for overdue materials',
+            'coust', 'description')
+       ),
+       ('bill.negative_balance_interval_on_lost',
+        'finance', 'interval',
+        oils_i18n_gettext(
+            'bill.negative_balance_interval_on_lost',
+            'Negative Balance Interval for Lost',
+            'coust', 'label'),
+        oils_i18n_gettext(
+            'bill.negative_balance_interval_on_lost',
+            'Amount of time after which no negative balances or credits are allowed on bills for lost/long overdue materials',
+            'coust', 'description')
+       );
+
+COMMIT;