From: Jason Stephenson Date: Mon, 12 Aug 2013 14:56:42 +0000 (-0400) Subject: Add settings for conditional negative balances. X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=1b2f3da30699cb042416d64569b9c0286a592a3d;p=evergreen%2Fequinox.git Add settings for conditional negative balances. 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 --- diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index c9500bf593..0f2664bc2f 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -3438,7 +3438,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA - + diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm index d958778565..0bba096d92 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/CircCommon.pm @@ -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; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm index 9fe8bb3d2e..99a714dc33 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Money.pm @@ -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); } diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql index d482914e06..8724b665b0 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -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 index 0000000000..3e363915d3 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.conditional_negative_balance.sql @@ -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;