Add settings for conditional negative balances.
authorJason Stephenson <jason@sigio.com>
Mon, 12 Aug 2013 14:56:42 +0000 (10:56 -0400)
committerJason Stephenson <jason@sigio.com>
Thu, 7 Nov 2013 22:00:42 +0000 (17:00 -0500)
This commit 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

Signed-off-by: Jason Stephenson <jason@sigio.com>
Open-ILS/examples/fm_IDL.xml
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/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.data.conditional_negative_balance.sql [new file with mode: 0644]

index c9500bf..0f2664b 100644 (file)
@@ -3438,7 +3438,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <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"/>
+                       <link field="billing" reltype="might_have" key="id" map="" 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">
index d958778..0bba096 100644 (file)
@@ -8,6 +8,7 @@ use OpenILS::Event;
 use OpenSRF::Utils::Logger qw(:logger);
 use OpenILS::Utils::CStoreEditor q/:funcs/;
 use OpenILS::Const qw/:const/;
+use List::MoreUtils qw(uniq);
 
 my $U = "OpenILS::Application::AppUtils";
 
@@ -17,7 +18,7 @@ my $U = "OpenILS::Application::AppUtils";
 
 
 # -----------------------------------------------------------------
-# Voids overdue fines on the given circ.  if a backdate is 
+# 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
 # overdue fines.
@@ -25,9 +26,9 @@ my $U = "OpenILS::Application::AppUtils";
 sub void_overdues {
     my($class, $e, $circ, $backdate, $note) = @_;
 
-    my $bill_search = { 
-        xact => $circ->id, 
-        btype => 1 
+    my $bill_search = {
+        xact => $circ->id,
+        btype => 1
     };
 
     if( $backdate ) {
@@ -38,7 +39,7 @@ sub void_overdues {
         # voiding fines that were applicable before the backdate.
         # ------------------------------------------------------------------
 
-        # if there is a raw time component (e.g. from postgres), 
+        # if there is a raw time component (e.g. from postgres),
         # turn it into an interval that interval_to_seconds can parse
         my $duration = $circ->fine_interval;
         $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
@@ -56,25 +57,12 @@ sub void_overdues {
         }
     }
 
-    my $bills = $class->oustanding_bills_for_xact($e, $circ, $bill_search);
-
-    # Sum any outstanding overdue billings
-    my $outstanding_overdues = 0;
-
-    foreach my $bill (@$bills) {
-        $outstanding_overdues = ($outstanding_overdues*100 + $bill->amount*100)/100;
-    }
-
-    if ($outstanding_overdues >= 0.01) {
-        # Make void payment
-        my $payobj = Fieldmapper::money::void_payment->new;
-        $payobj->amount($outstanding_overdues);
-        $payobj->amount_collected($outstanding_overdues);
-        $payobj->xact($circ->id);
-        $payobj->note($note);
-        $payobj->accepting_usr($e->requestor->id);
-    
-        $e->create_money_void_payment($payobj) 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->authtoken, $billids, $note);
+        if (ref($result)) {
+            return $result;
+        }
     }
 
     return undef;
@@ -264,29 +252,55 @@ sub can_close_circ {
 
 # -----------------------------------------------------------------
 # Given an editor and a xact, 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.
+# 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:
 #
-# It also takes an optional last parameter as a bill search predicate
-# filter.
+# 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
 #
-# This function is adapted from code written by Jeff Godin of Traverse
-# Area District Library and submitted on LaunchPad bug #1009049.
+# 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.
+#
+# NOTE: The original bill amount is considered the original bill
+# amount minus any amount of void payment linked to that bill.  Void
+# payment not applied to any particular billing are handled like
+# regular payments.
+#
+# I wonder if it would not be better to just summarize the billings by
+# type?
+#
+# This function is heavily adapted from code written by Jeff Godin of
+# Traverse Area District Library and submitted on LaunchPad bug
+# #1009049.
 # -----------------------------------------------------------------
-sub outstanding_bills_for_xact {
-    my ($class, $e, $xact, $bill_predicate) = @_;
+sub bill_payment_map_for_xact {
+    my ($class, $e, $xact) = @_;
 
-    # A little defensive coding never hurts.
-    unless ($bill_predicate) {
-        $bill_predicate = {xact => $xact->id};
-    } else {
-        $bill_predicate->{xact} = $xact->id unless ($bill_predicate->{xact});
-    }
-
-    # find all unvoided bills in order
+    # find all bills in order
     my $bill_search = [
-        $bill_predicate,
+        {xact => $xact->id()},
         { order_by => { mb => { billing_ts => { direction => 'asc' } } } },
     ];
 
@@ -296,33 +310,289 @@ sub outstanding_bills_for_xact {
         { order_by => { mp => { payment_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 void_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);
 
-    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;
-
-        while ($paybal > 0) {
-            # get next billing
-            my $bill = shift @{$bills};
-            my $newbal = (($paybal*100) - ($bill->amount*100))/100;
-            if ($newbal < 0) {
-                $newbal = 0;
-                my $new_bill_amount = (($bill->amount*100) - ($paybal*100))/100;
-                $bill->amount($new_bill_amount);
-                unshift(@{$bills}, $bill); # put the partially-paid bill back on top of the stack
+    # 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;
+
+    # Retrieve the payments.  Flesh voids do that we don't have to
+    # retrieve them later.
+    my $payments = $e->search_money_payment(
+        [
+            $payment_search,
+            {
+                flesh=>1,
+                flesh_fields => {
+                    mp => ['void_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 {$_->void_payment()} grep {$_->payment_type() eq 'void_payment' && $_->void_payment()->billing() == $bill->id()} @$payments;
+        if (@voids) {
+            foreach my $void (@voids) {
+                my $new_amount = ($bill->amount() * 100 - $void->amount() * 100) / 100;
+                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 = ($bill->amount() * 100 - $payment->amount() * 100) / 100;
+                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 an
+# authtoken, an arrayref of bill ids or bills, and an optional note.
+sub real_void_bills {
+    my ($class, $authtoken, $billids, $note) = @_;
+
+    # Get an editor and see if we have permission to void bills.
+    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;
+
+    # 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);
+        my $isgrocery = ($mbt->grocery()) ? 1 : 0;
+        if ($isgrocery) {
+            # We don't actually use this, yet, but just in case.
+            $grocery = $mbt->grocery();
+        } else {
+            $circ = $mbt->circulation();
+            $copy = $circ->target_copy();
+        }
+
+        # Retrieve settings based on transaction location and copy
+        # location if we have a circulation.
+        my ($neg_balance_default, $neg_balance_overdues,
+            $neg_balance_lost, $neg_balance_interval_default,
+            $neg_balance_interval_overdues, $neg_balance_interval_lost);
+        if (!$isgrocery) {
+            # defaults and overdue settings come from transaction org unit.
+            $neg_balance_default = $U->ou_ancestor_setting(
+                $circ->circ_lib(), 'circ.prohibit_negative_balance_default');
+            $neg_balance_overdues = $U->ou_ancestor_setting(
+                $circ->circ_lib(), 'circ.prohibit_negative_balance_on_overdues');
+            $neg_balance_interval_default = $U->ou_ancestor_setting(
+                $circ->circ_lib(), 'circ.negative_balance_interval_default');
+            $neg_balance_interval_overdues = $U->ou_ancestor_setting(
+                $circ->circ_lib(), 'circ.negative_balance_interval_on_overdues');
+            # settings for lost come from copy circlib.
+            $neg_balance_lost = (
+                $U->ou_ancestor_setting($copy->circ_lib(), 'circ.prohibit_negative_balance_on_lost')
+                ||
+                $U->ou_ancestor_setting($copy->circ_lib(), 'circ.prohibit_negative_balance_default')
+            );
+            $neg_balance_interval_lost = (
+                $U->ou_ancestor_setting($copy->circ_lib(), 'circ.negative_balance_interval_on_lost')
+                ||
+                $U->ou_ancestor_setting($copy->circ_lib(), 'circ.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;
+
+            # 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 ($bpentry->{void_amount} > 0 && $bpentry->{void_amount} == $bpentry->{bill_amount}) {
+                $e->rollback;
+                return OpenILS::Event->new('BILL_ALREADY_VOIDED', payload => $bill);
+            }
+
+            # We'll use this variable to determine if we need a void
+            # and how much the void payment amount should be.
+            my $amount_to_void = 0;
+
+            # None of our new settings apply to grocery bills, so
+            # we'll just void them, regardless of balances, etc.
+            if ($isgrocery) {
+                $amount_to_void = $bpentry->{bill}->amount();
+            } else {
+                # Hang on tight.  It's about to get hairy.
+                
+            }
+
+            # Create the void payment if necessary:
+            if ($amount_to_void > 0) {
+                my $payobj = Fieldmapper::money::void_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_void_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 = ($bpentry->{bill}->amount() * 100 - $amount_to_void * 100) / 100;
+                $bpentry->{bill}->amount($new_bill_amount);
             }
-            $paybal = $newbal;
         }
 
-    } @$payments;
+        my $org = $U->xact_org($xactid, $e);
+        $users{$mbt->usr} = {} unless $users{$mbt->usr};
+        $users{$mbt->usr}->{$org} = 1;
 
-    return $bills;
+        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);
+        }
+    }
+    $e->commit;
+    return 1;
 }
 
 1;
index 9fe8bb3..99a714d 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;
@@ -349,7 +351,7 @@ sub make_payments {
             # close is 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)) {
@@ -739,59 +741,7 @@ __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;
-
-        my $amount_to_void = $bill->amount;
-        my $open_amount = 0;
-        my $open_bills = OpenILS::Application::Circ::CircCommon->outstanding_bills_for_xact($e, $xact);
-        for my $open_bill (@$open_bills) {
-            $open_amount = ($open_amount*100 + $open_bill->amount*100)/100;
-        }
-
-        if($open_amount == 0) {
-            $e->rollback;
-            return OpenILS::Event->new('BILL_ALREADY_VOIDED', payload => $bill);
-        } else {
-            # Void the lesser of $amount_to_void or $open_amount:
-            $amount_to_void = ($amount_to_void < $open_amount) ? $amount_to_void : $open_amount;
-            # Make a void payment in the amount of $amount_to_void
-            my $payobj = Fieldmapper::money::void_payment->new;
-            $payobj->amount($amount_to_void);
-            $payobj->amount_collected($amount_to_void);
-            $payobj->xact($bill->xact);
-            $payobj->accepting_usr($e->requestor->id);
-
-            $e->create_money_void_payment($payobj) or return $e->die_event;
-        }
-
-        my $org = $U->xact_org($bill->xact, $e);
-        $users{$xact->usr} = {} unless $users{$xact->usr};
-        $users{$xact->usr}->{$org} = 1;
-
-        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);
-        }
-    }
-    $e->commit;
-    return 1;
+    return $CC->real_void_bills($authtoken, \@billids);
 }
 
 
index d482914..8724b66 100644 (file)
@@ -4921,7 +4921,6 @@ INSERT into config.org_unit_setting_type
         'description'
     ),
     'string', null)
-
 ,( 'circ.patron_edit.duplicate_patron_check_depth', 'circ',
     oils_i18n_gettext(
         'circ.patron_edit.duplicate_patron_check_depth',
@@ -4934,6 +4933,66 @@ INSERT into config.org_unit_setting_type
         'coust',
         'description'),
     'integer', 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-overde 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..3e36391
--- /dev/null
@@ -0,0 +1,76 @@
+BEGIN;
+
+-- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+-- 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;