Implemented a mechanism to track customer overpayments (credits). Also implemented a reconciling algorithm, matching payments to charges. Preliminary testing seems to show that it works well. More thorough testing required.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716@392 97e9348a-65ac-dc4b-aefc-98561f571b83
This commit is contained in:
@@ -1074,7 +1074,8 @@ CREATE TABLE `pmgr_statement_entries` (
|
|||||||
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
|
||||||
`type` ENUM('CHARGE',
|
`type` ENUM('CHARGE',
|
||||||
'PAYMENT')
|
'PAYMENT',
|
||||||
|
'CREDIT')
|
||||||
NOT NULL,
|
NOT NULL,
|
||||||
|
|
||||||
`transaction_id` INT(10) UNSIGNED NOT NULL,
|
`transaction_id` INT(10) UNSIGNED NOT NULL,
|
||||||
@@ -1092,7 +1093,7 @@ CREATE TABLE `pmgr_statement_entries` (
|
|||||||
-- transaction. Keeping it here anyway, for simplicity. If it's
|
-- transaction. Keeping it here anyway, for simplicity. If it's
|
||||||
-- truly redundant, and unnecessary, we can always re
|
-- truly redundant, and unnecessary, we can always re
|
||||||
`customer_id` INT(10) UNSIGNED NOT NULL,
|
`customer_id` INT(10) UNSIGNED NOT NULL,
|
||||||
`lease_id` INT(10) UNSIGNED NOT NULL,
|
`lease_id` INT(10) UNSIGNED DEFAULT NULL,
|
||||||
|
|
||||||
`amount` FLOAT(12,2) NOT NULL,
|
`amount` FLOAT(12,2) NOT NULL,
|
||||||
|
|
||||||
|
|||||||
@@ -1032,6 +1032,23 @@ foreach $row (@{query($sdbh, $query)}) {
|
|||||||
dates('charge', $row->{'ChargeDate'}, $row->{'EndDate'},
|
dates('charge', $row->{'ChargeDate'}, $row->{'EndDate'},
|
||||||
$row->{'ChargeDescription'}, $row->{'LedgerID'});
|
$row->{'ChargeDescription'}, $row->{'LedgerID'});
|
||||||
|
|
||||||
|
# Fix Brenda Harmon bug
|
||||||
|
$row->{'ChargeAmount'} = 50
|
||||||
|
if ($row->{'ChargeID'} == 19);
|
||||||
|
|
||||||
|
# # REVISIT <AP>: 20090726
|
||||||
|
# # Temporary testing of customer credits
|
||||||
|
# $row->{'ChargeAmount'} = 49
|
||||||
|
# if ($row->{'ChargeID'} == 19);
|
||||||
|
# #next if $row->{'ChargeID'} == 3777;
|
||||||
|
# #next if $row->{'ChargeID'} == 3838;
|
||||||
|
# $row->{'ChargeAmount'} = .80
|
||||||
|
# if $row->{'ChargeID'} == 3777;
|
||||||
|
# $row->{'ChargeAmount'} = .15
|
||||||
|
# if $row->{'ChargeID'} == 3838;
|
||||||
|
# # END REVISIT <AP>: 20090726
|
||||||
|
|
||||||
|
|
||||||
$newdb{'lookup'}{'charge'}{$row->{'ChargeID'}}{'amount'}
|
$newdb{'lookup'}{'charge'}{$row->{'ChargeID'}}{'amount'}
|
||||||
= $row->{'ChargeAmount'};
|
= $row->{'ChargeAmount'};
|
||||||
$newdb{'lookup'}{'charge'}{$row->{'ChargeID'}}{'customer_id'}
|
$newdb{'lookup'}{'charge'}{$row->{'ChargeID'}}{'customer_id'}
|
||||||
@@ -1215,6 +1232,7 @@ foreach $row (@{query($sdbh, $query)}) {
|
|||||||
'account_id' => $newdb{'lookup'}{'account'}{'A/R'}{'account_id'},
|
'account_id' => $newdb{'lookup'}{'account'}{'A/R'}{'account_id'},
|
||||||
'ledger_id' => $newdb{'lookup'}{'account'}{'A/R'}{'ledger_id'},
|
'ledger_id' => $newdb{'lookup'}{'account'}{'A/R'}{'ledger_id'},
|
||||||
'crdr' => 'CREDIT',
|
'crdr' => 'CREDIT',
|
||||||
|
'comment' => "Receipt: $row->{'ReceiptNum'}; Type: $row->{'PaymentType'}",
|
||||||
});
|
});
|
||||||
|
|
||||||
$newdb{'lookup'}{'receipt'}{$row->{'ReceiptNum'}}{$row->{'PaymentType'}}{'receipt_id'}
|
$newdb{'lookup'}{'receipt'}{$row->{'ReceiptNum'}}{$row->{'PaymentType'}}{'receipt_id'}
|
||||||
@@ -1286,7 +1304,28 @@ foreach $row (@{query($sdbh, $query)}) {
|
|||||||
$newdb{'lookup'}{'payment'} = {};
|
$newdb{'lookup'}{'payment'} = {};
|
||||||
|
|
||||||
$query = "SELECT * FROM Payments ORDER BY PaymentID";
|
$query = "SELECT * FROM Payments ORDER BY PaymentID";
|
||||||
foreach $row (@{query($sdbh, $query)})
|
foreach $row (@{query($sdbh, $query)},
|
||||||
|
## Special case - Fix sitelink Brenda Harmon bug
|
||||||
|
( 0 ?
|
||||||
|
{ 'PaymentDate' => '4/1/2009 05:01',
|
||||||
|
'ReceiptNum' => 10,
|
||||||
|
'PaymentType' => 2,
|
||||||
|
'PaymentID' => 99991,
|
||||||
|
'ChargeID' => 3777,
|
||||||
|
'PaymentAmount' => 1,
|
||||||
|
'Memo' => 'Utilized credit left by $1 overpayment',
|
||||||
|
} : ()),
|
||||||
|
( 0 ?
|
||||||
|
{ 'PaymentDate' => '4/1/2009 05:01',
|
||||||
|
'ReceiptNum' => 10,
|
||||||
|
'PaymentType' => 2,
|
||||||
|
'PaymentID' => 99991,
|
||||||
|
'ChargeID' => 19,
|
||||||
|
'PaymentAmount' => 1.00,
|
||||||
|
'entry_type' => 'CREDIT',
|
||||||
|
'Memo' => 'Overpayment left $1 credit',
|
||||||
|
} : ()),
|
||||||
|
)
|
||||||
{
|
{
|
||||||
my (undef, $effective_date, $through_date) =
|
my (undef, $effective_date, $through_date) =
|
||||||
dates('payment', $row->{'PaymentDate'});
|
dates('payment', $row->{'PaymentDate'});
|
||||||
@@ -1302,13 +1341,18 @@ foreach $row (@{query($sdbh, $query)})
|
|||||||
# 'effective_date' => $newdb{'lookup'}{'receipt'}{$row->{'ReceiptNum'}}{$row->{'PaymentType'}}{'effective_date'},
|
# 'effective_date' => $newdb{'lookup'}{'receipt'}{$row->{'ReceiptNum'}}{$row->{'PaymentType'}}{'effective_date'},
|
||||||
# 'effective_date' => $effective_date;
|
# 'effective_date' => $effective_date;
|
||||||
# 'through_date' => $through_date;
|
# 'through_date' => $through_date;
|
||||||
'lease_id' => $newdb{'lookup'}{'charge'}{$row->{'ChargeID'}}{'lease_id'},
|
'lease_id' => ($row->{'entry_type'} eq 'CREDIT'
|
||||||
|
? 0
|
||||||
|
: $newdb{'lookup'}{'charge'}{$row->{'ChargeID'}}{'lease_id'}),
|
||||||
'customer_id' => $newdb{'lookup'}{'charge'}{$row->{'ChargeID'}}{'customer_id'},
|
'customer_id' => $newdb{'lookup'}{'charge'}{$row->{'ChargeID'}}{'customer_id'},
|
||||||
'amount' => $reconcile_amount,
|
'amount' => $reconcile_amount,
|
||||||
|
|
||||||
'account_id' => $newdb{'lookup'}{'receipt'}{$row->{'ReceiptNum'}}{$row->{'PaymentType'}}{'credit_account_id'},
|
'account_id' => $newdb{'lookup'}{'receipt'}{$row->{'ReceiptNum'}}{$row->{'PaymentType'}}{'credit_account_id'},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$row->{'ChargeID'} = undef
|
||||||
|
if $row->{'entry_type'} eq 'CREDIT';
|
||||||
|
|
||||||
# Update the receipt customer_id, now that we have payment info
|
# Update the receipt customer_id, now that we have payment info
|
||||||
$newdb{'tables'}{'transactions'}{'rows'}[
|
$newdb{'tables'}{'transactions'}{'rows'}[
|
||||||
$newdb{'lookup'}{'payment'}{$row->{'PaymentID'}}{'receipt_id'}
|
$newdb{'lookup'}{'payment'}{$row->{'PaymentID'}}{'receipt_id'}
|
||||||
@@ -1316,11 +1360,12 @@ foreach $row (@{query($sdbh, $query)})
|
|||||||
|
|
||||||
# Use the Memo as our comment, if it exists
|
# Use the Memo as our comment, if it exists
|
||||||
my $comment = $row->{'Memo'} || "Payment: $row->{'ReceiptNum'}; Type: $row->{'PaymentType'}";
|
my $comment = $row->{'Memo'} || "Payment: $row->{'ReceiptNum'}; Type: $row->{'PaymentType'}";
|
||||||
|
$comment = "Payment: $row->{'ReceiptNum'}; Type: $row->{'PaymentType'}; Charge: $row->{'ChargeID'}";
|
||||||
|
|
||||||
# Add the Payment Statement Entry
|
# Add the Payment Statement Entry
|
||||||
addRow('statement_entries', {
|
addRow('statement_entries', {
|
||||||
'transaction_id' => $newdb{'lookup'}{'payment'}{$row->{'PaymentID'}}{'receipt_id'},
|
'transaction_id' => $newdb{'lookup'}{'payment'}{$row->{'PaymentID'}}{'receipt_id'},
|
||||||
'type' => 'PAYMENT',
|
'type' => $row->{'entry_type'} || 'PAYMENT',
|
||||||
# 'effective_date' => $newdb{'lookup'}{'payment'}{$row->{'PaymentID'}}{'effective_date'},
|
# 'effective_date' => $newdb{'lookup'}{'payment'}{$row->{'PaymentID'}}{'effective_date'},
|
||||||
# 'through_date' => $newdb{'lookup'}{'payment'}{$row->{'PaymentID'}}{'through_date'},
|
# 'through_date' => $newdb{'lookup'}{'payment'}{$row->{'PaymentID'}}{'through_date'},
|
||||||
'effective_date' => $effective_date,
|
'effective_date' => $effective_date,
|
||||||
@@ -1353,16 +1398,6 @@ if ($newdb{'lookup'}{'_closing'}{'credit_entry_id'}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
######################################################################
|
|
||||||
## Special cases - Fix sitelink Brenda Harmon bug
|
|
||||||
# print("Special Cases - Fix Brenda Harmon...\n");
|
|
||||||
# $query =
|
|
||||||
# "UPDATE pmgr_contacts" .
|
|
||||||
# " SET first_name = 'Krystan'" .
|
|
||||||
# " WHERE first_name = 'Kristan' AND last_name = 'Mancini'";
|
|
||||||
# query($db_handle, $query);
|
|
||||||
|
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
## Special case - Equities / Loans / Petty Cash
|
## Special case - Equities / Loans / Petty Cash
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,9 @@ class AppModel extends Model {
|
|||||||
$query['link'] = array();
|
$query['link'] = array();
|
||||||
if (!$link && !isset($query['contain']))
|
if (!$link && !isset($query['contain']))
|
||||||
$query['contain'] = array();
|
$query['contain'] = array();
|
||||||
|
|
||||||
|
// In case caller expects query to come back
|
||||||
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -395,7 +395,7 @@ class Customer extends AppModel {
|
|||||||
$ar_transaction_stats += $ar_transaction_stats['LedgerEntry'];
|
$ar_transaction_stats += $ar_transaction_stats['LedgerEntry'];
|
||||||
pr(compact('ar_transaction_stats'));
|
pr(compact('ar_transaction_stats'));
|
||||||
|
|
||||||
$stats = $statement_stats;
|
$stats = $ar_transaction_stats;
|
||||||
return $stats;
|
return $stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class StatementEntry extends AppModel {
|
|||||||
($sum ? ')' : '') . ' AS charge' . ($sum ? 's' : ''),
|
($sum ? ')' : '') . ' AS charge' . ($sum ? 's' : ''),
|
||||||
|
|
||||||
($sum ? 'SUM(' : '') .
|
($sum ? 'SUM(' : '') .
|
||||||
"IF({$entry_name}.type = 'PAYMENT'," .
|
"IF({$entry_name}.type = 'PAYMENT' OR {$entry_name}.type = 'CREDIT'," .
|
||||||
" {$entry_name}.amount, NULL)" .
|
" {$entry_name}.amount, NULL)" .
|
||||||
($sum ? ')' : '') . ' AS payment' . ($sum ? 's' : ''),
|
($sum ? ')' : '') . ' AS payment' . ($sum ? 's' : ''),
|
||||||
|
|
||||||
@@ -347,6 +347,203 @@ OPTION 2
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**************************************************************************
|
||||||
|
**************************************************************************
|
||||||
|
**************************************************************************
|
||||||
|
* function: assignCredits
|
||||||
|
* - Assigns all credits to existing charges
|
||||||
|
*/
|
||||||
|
function assignCredits($query = null, $receipt_id = null) {
|
||||||
|
pr(array('StatementEntry::assignCredits' => compact('query')));
|
||||||
|
$this->queryInit($query);
|
||||||
|
|
||||||
|
// First, find all known credits
|
||||||
|
$lquery = $query;
|
||||||
|
$lquery['conditions'][] = array('StatementEntry.type' => 'CREDIT');
|
||||||
|
$credits = $this->find('all', $lquery);
|
||||||
|
pr(compact('lquery', 'credits'));
|
||||||
|
|
||||||
|
// Then, find all receipts that have not had all
|
||||||
|
// monies dispursed for either payments or credits
|
||||||
|
// REVISIT <AP>: If we implement CREDITS as we're
|
||||||
|
// anticipating, then this concept of "anonymous"
|
||||||
|
// credits won't exists (i.e. credits that are
|
||||||
|
// not explicitly specified with a statement entry
|
||||||
|
// of type CREDIT). All transactions MUST balance
|
||||||
|
// out to the sum of their statement entries, so
|
||||||
|
// we'll be able to just delete all the anon_credit
|
||||||
|
// code.
|
||||||
|
$lquery = $query;
|
||||||
|
$lquery['link'] = array('StatementEntry' => array('fields' => array()) + $lquery['link']);
|
||||||
|
$lquery['conditions'][] = array('Transaction.type' => 'RECEIPT');
|
||||||
|
$lquery['fields'] = array('Transaction.id', 'Transaction.stamp', 'Transaction.amount',
|
||||||
|
//'SUM(StatementEntry.amount) AS applied_amount',
|
||||||
|
'Transaction.amount - SUM(StatementEntry.amount) AS balance');
|
||||||
|
$lquery['group'] = 'Transaction.id HAVING balance > 0';
|
||||||
|
$anon_credits = $this->Transaction->find('all', $lquery);
|
||||||
|
foreach ($anon_credits AS &$ac) {
|
||||||
|
$ac['Transaction'] += $ac[0];
|
||||||
|
unset($ac[0]);
|
||||||
|
}
|
||||||
|
pr(compact('lquery', 'anon_credits'));
|
||||||
|
|
||||||
|
// REVISIT <AP>: 20090726
|
||||||
|
// This algorithm shouldn't be hardcoded. We need to allow
|
||||||
|
// the user to specify how payments should be applied.
|
||||||
|
|
||||||
|
// Now find all unpaid charges
|
||||||
|
$lquery = $query;
|
||||||
|
$lquery['order'] = 'StatementEntry.effective_date ASC';
|
||||||
|
$charges = $this->reconciledSet('CHARGE', $query, true);
|
||||||
|
pr(compact('lquery', 'charges'));
|
||||||
|
|
||||||
|
// Initialize our list of used credits
|
||||||
|
$used_credits = array();
|
||||||
|
$used_anon_credits = array();
|
||||||
|
|
||||||
|
// Work through all unpaid charges, applying payments as we go
|
||||||
|
foreach ($charges['entries'] AS $charge) {
|
||||||
|
|
||||||
|
pr(array('StatementEntry::assignCredits' =>
|
||||||
|
array('checkpoint' => 'Process Charge')
|
||||||
|
+ compact('charge')));
|
||||||
|
|
||||||
|
// Check that we have available credits.
|
||||||
|
// Technically, this isn't necessary, since the loop
|
||||||
|
// will handle everything just fine. However, this
|
||||||
|
// just saves extra processing if/when there is no
|
||||||
|
// means to resolve a charge anyway.
|
||||||
|
if (count($credits) == 0 && count($anon_credits) == 0) {
|
||||||
|
pr(array('StatementEntry::assignCredits' =>
|
||||||
|
array('checkpoint' => 'No available credits')));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$charge['balance'] = $charge['StatementEntry']['balance'];
|
||||||
|
while ($charge['balance'] > 0 &&
|
||||||
|
(count($credits) || count($anon_credits))) {
|
||||||
|
|
||||||
|
pr(array('StatementEntry::assignCredits' =>
|
||||||
|
array('checkpoint' => 'Attempt Charge Reconciliation')
|
||||||
|
+ compact('charge')));
|
||||||
|
|
||||||
|
// Use explicit credits before using implicit credits
|
||||||
|
// (Not sure it matters though).
|
||||||
|
if (count($credits)) {
|
||||||
|
// Peel off the first credit available
|
||||||
|
$credit =& $credits[0];
|
||||||
|
$payment_date = $credit['StatementEntry']['effective_date'];
|
||||||
|
$payment_transaction_id = $credit['StatementEntry']['transaction_id'];
|
||||||
|
|
||||||
|
if (!isset($credit['balance']))
|
||||||
|
$credit['balance'] = $credit['StatementEntry']['amount'];
|
||||||
|
}
|
||||||
|
elseif (count($anon_credits)) {
|
||||||
|
// Peel off the first credit available
|
||||||
|
$credit =& $anon_credits[0];
|
||||||
|
$payment_date = $credit['Transaction']['stamp'];
|
||||||
|
$payment_transaction_id = $credit['Transaction']['id'];
|
||||||
|
|
||||||
|
if (!isset($credit['balance']))
|
||||||
|
$credit['balance'] = $credit['Transaction']['balance'];
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
die("HOW DID WE GET HERE WITH NO CREDITS?");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the payment amount to the maximum amount
|
||||||
|
// possible without exceeding the charge or credit balance
|
||||||
|
$payment_amount = min($charge['balance'], $credit['balance']);
|
||||||
|
if (!isset($credit['applied']))
|
||||||
|
$credit['applied'] = 0;
|
||||||
|
|
||||||
|
$credit['applied'] += $payment_amount;
|
||||||
|
$credit['balance'] -= $payment_amount;
|
||||||
|
|
||||||
|
pr(array('StatementEntry::assignCredits' =>
|
||||||
|
array('checkpoint' => (($credit['balance'] > 0 ? 'Utilized' : 'Exhausted')
|
||||||
|
. (count($credits) ? '' : ' Anon')
|
||||||
|
. ' Credit'))
|
||||||
|
+ compact('credit')));
|
||||||
|
|
||||||
|
if ($credit['balance'] < 0)
|
||||||
|
die("HOW DID WE END UP WITH NEGATIVE CREDIT BALANCE?");
|
||||||
|
|
||||||
|
// If we've exhaused the credit, get it out of the
|
||||||
|
// available credit pool (but keep track of it for later).
|
||||||
|
if ($credit['balance'] <= 0) {
|
||||||
|
if (count($credits))
|
||||||
|
$used_credits[] = array_shift($credits);
|
||||||
|
else
|
||||||
|
$used_anon_credits[] = array_shift($anon_credits);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a payment that uses the available credit to pay the charge
|
||||||
|
$payment = array('type' => 'PAYMENT',
|
||||||
|
'account_id' => $this->Account->accountReceivableAccountID(),
|
||||||
|
'amount' => $payment_amount,
|
||||||
|
'effective_date' => $payment_date,
|
||||||
|
'transaction_id' => $payment_transaction_id,
|
||||||
|
'customer_id' => $charge['StatementEntry']['customer_id'],
|
||||||
|
'lease_id' => $charge['StatementEntry']['lease_id'],
|
||||||
|
'charge_entry_id' => $charge['StatementEntry']['id'],
|
||||||
|
'comment' => null,
|
||||||
|
);
|
||||||
|
|
||||||
|
pr(array('StatementEntry::assignCredits' =>
|
||||||
|
array('checkpoint' => 'New Payment Entry')
|
||||||
|
+ compact('payment')));
|
||||||
|
|
||||||
|
$SE = new StatementEntry();
|
||||||
|
$SE->create();
|
||||||
|
if (!$SE->save($payment))
|
||||||
|
die("UNABLE TO SAVE NEW PAYMENT ENTRY");
|
||||||
|
|
||||||
|
// Adjust the charge balance to reflect the new payment
|
||||||
|
$charge['balance'] -= $payment_amount;
|
||||||
|
if ($charge['balance'] < 0)
|
||||||
|
die("HOW DID WE GET A NEGATIVE CHARGE AMOUNT?");
|
||||||
|
|
||||||
|
if ($charge['balance'] <= 0)
|
||||||
|
pr(array('StatementEntry::assignCredits' =>
|
||||||
|
array('checkpoint' => 'Fully Paid Charge')));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make partially used credits are added to the used list
|
||||||
|
if (isset($credits[0]['applied']))
|
||||||
|
$used_credits[] = array_shift($credits);
|
||||||
|
if (isset($anon_credits[0]['applied']))
|
||||||
|
$used_anon_credits[] = array_shift($anon_credits);
|
||||||
|
|
||||||
|
pr(array('StatementEntry::assignCredits' =>
|
||||||
|
array('checkpoint' => 'Payments added')
|
||||||
|
+ compact('credits', 'used_credits', 'anon_credits', 'used_anon_credits')));
|
||||||
|
|
||||||
|
// Finally, clean up any credits that have been used
|
||||||
|
foreach ($used_credits AS $credit) {
|
||||||
|
if ($credit['balance'] > 0) {
|
||||||
|
pr(array('StatementEntry::assignCredits' =>
|
||||||
|
array('checkpoint' => 'Update Credit Entry')
|
||||||
|
+ compact('credit')));
|
||||||
|
|
||||||
|
$this->id = $credit['StatementEntry']['id'];
|
||||||
|
$this->saveField('amount', $credit['balance']);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pr(array('StatementEntry::assignCredits' =>
|
||||||
|
array('checkpoint' => 'Delete Exhausted Credit Entry')
|
||||||
|
+ compact('credit')));
|
||||||
|
|
||||||
|
$this->del($credit['StatementEntry']['id'], false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**************************************************************************
|
/**************************************************************************
|
||||||
**************************************************************************
|
**************************************************************************
|
||||||
**************************************************************************
|
**************************************************************************
|
||||||
|
|||||||
Reference in New Issue
Block a user