Merge in from surplus_account_20090815 r592

git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@593 97e9348a-65ac-dc4b-aefc-98561f571b83
This commit is contained in:
abijah
2009-08-17 02:22:54 +00:00
parent 893fc74c0a
commit 23cdd0e497
22 changed files with 629 additions and 433 deletions

View File

@@ -7,6 +7,13 @@ class Transaction extends AppModel {
'Ledger',
);
var $hasOne = array(
'NsfTender' => array(
'className' => 'Tender',
'foreignKey' => 'nsf_transaction_id',
),
);
var $hasMany = array(
'LedgerEntry' => array(
'dependent' => true,
@@ -44,6 +51,7 @@ class Transaction extends AppModel {
var $default_log_level = array('log' => 30, 'show' => 15);
//var $max_log_level = 10;
/**************************************************************************
**************************************************************************
@@ -75,7 +83,6 @@ class Transaction extends AppModel {
// Go through the statement entries and flag as charges
foreach ($data['Entry'] AS &$entry)
$entry += array('type' => 'CHARGE',
'crdr' => 'CREDIT',
);
$ids = $this->addTransaction($data['control'], $data['Transaction'], $data['Entry']);
@@ -117,7 +124,6 @@ class Transaction extends AppModel {
// Go through the statement entries and flag as disbursements
foreach ($data['Entry'] AS &$entry)
$entry += array('type' => 'DISBURSEMENT', // not used
'crdr' => 'DEBIT',
'account_id' =>
(isset($entry['Tender']['tender_type_id'])
? ($this->LedgerEntry->Tender->TenderType->
@@ -237,7 +243,6 @@ class Transaction extends AppModel {
if (!isset($group[$entry['account_id']]))
$group[$entry['account_id']] =
array('account_id' => $entry['account_id'],
'crdr' => strtoupper($this->Account->fundamentalOpposite($data['Transaction']['crdr'])),
'amount' => 0);
$group[$entry['account_id']]['amount'] += $entry['amount'];
}
@@ -392,7 +397,6 @@ class Transaction extends AppModel {
// Go through the statement entries and flag as payments
foreach ($data['Entry'] AS &$entry)
$entry += array('type' => 'PAYMENT',
'crdr' => 'CREDIT',
);
$ids = $this->addTransaction($data['control'], $data['Transaction'], $data['Entry']);
@@ -453,59 +457,203 @@ class Transaction extends AppModel {
* entries, as layed out in the $data['Entry'] array. The array is
* overloaded, since it is used to create both ledger _and_ statement
* entries.
*
* $data
* - Transaction
* - [MANDATORY]
* - type (INVOICE, RECEIPT)
* - account_id
* - crdr
* - [OPTIONAL]
* - stamp
* (default: NOW)
* - comment
* - [AUTOMATICALLY SET] (if set, these items will be overwritten)
* - id
* - amount
* - customer_id
* - ledger_id
*
* - Entry (array)
* - [MANDATORY]
* - type (CHARGE, DISBURSEMENT)
* - account_id
* - crdr
* - amount
* - [OPTIONAL]
* - effective_date
* - through_date
* - due_date
* - comment (used for statement or ledger entry, based on context)
* - ledger_entry_comment
* - statement_entry_comment
* - Tender
* - [MANDATORY]
* - tender_type_id
* - [OPTIONAL]
* - name
* (default: Entry Account Name & data1)
* - data1, data2, data3, data4
* - comment
* - [AUTOMATICALLY SET] (if set, these items will be overwritten)
* - id
* - ledger_entry_id
* - deposit_transaction_id
* - nsf_transaction_id
* - [AUTOMATICALLY SET] (if set, these items will be overwritten)
* - id
* - transaction_id
* - ledger_id
*
*/
function addTransaction($control, $transaction, $entries) {
$this->prEnter(compact('control', 'transaction', 'entries'));
$result = $this->_splitEntries($control, $transaction, $entries);
if (!empty($result['error']))
return $this->prReturn(array('error' => true));
// Make use of the work done by splitEntries
$transaction = $this->filter_null($transaction) + $result['transaction'];
$entries = $result['entries'];
extract($result['vars']);
$this->pr(20, compact('transaction', 'entries'));
// Move forward, verifying and saving everything.
$ret = array('data' => $transaction);
if (!$this->verifyTransaction($transaction, $entries))
return $this->prReturn(array('error' => true) + $ret);
// Save transaction to the database
$this->create();
if (!$this->save($transaction))
return $this->prReturn(array('error' => true) + $ret);
$ret['transaction_id'] = $transaction['id'] = $this->id;
// Add the entries
$ret += $this->addTransactionEntries($control, $transaction, $entries, false);
// If the caller requests 'assign'=>true, they really
// want to do a credit assignment, and _then_ create
// an explicit credit with any leftover. If an array
// is specified, they get full control of the order.
if (empty($control['assign']))
$assign_ops = array();
elseif (is_array($control['assign']))
$assign_ops = $control['assign'];
elseif (is_bool($control['assign']))
$assign_ops = (empty($control['assign_receipt'])
? array('assign')
: array('assign', 'create'));
else
$this->INTERNAL_ERROR('Invalid control[assign] parameter');
$this->pr(17, compact('assign_ops'), 'Credit operations');
// Go through the requested assignment mechanisms
foreach ($assign_ops AS $method) {
if (!empty($ret['error']))
break;
$this->pr(17, compact('method'), 'Handling credits');
if ($method === 'assign') {
$result = $this->StatementEntry->assignCredits
(null,
(empty($control['assign_receipt']) ? null
: $ret['transaction_id']),
null,
$assign_disbursement_type,
$transaction['customer_id'],
$transaction['lease_id']
);
}
elseif ($method === 'create' || is_numeric($method)) {
if (is_numeric($method))
$credit_amount = $method;
else {
$stats = $this->stats($transaction['id']);
$credit_amount = $stats['undisbursed'];
}
if ($credit_amount < 0)
$this->INTERNAL_ERROR('Receipt has negative undisbursed balance');
if (empty($credit_amount))
continue;
$result = $this->addTransactionEntries
(array('include_ledger_entry' => true,
'include_statement_entry' => true),
array('crdr' => 'DEBIT') + $transaction,
array(array('type' => 'SURPLUS',
'account_id' => $this->Account->customerCreditAccountID(),
'amount' => $credit_amount,
),
));
}
else
$this->INTERNAL_ERROR('Invalid assign method');
$ret['credit'][$method] = $result;
if (!empty($result['error']))
$ret['error'] = true;
}
$this->Customer->update($transaction['customer_id']);
return $this->prReturn($ret);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: addTransactionEntries
* - Largely a helper function to addTransaction, this function is
* responsible for adding ledger/statement entries to an existing
* transaction. If needed, this function can also be called outside
* of addTransaction, although it's not clear where that would be
* appropriate, since transactions are really snapshots of some
* event, and shouldn't be mucked with after creation.
*/
function addTransactionEntries($control, $transaction, $entries, $split = true) {
$this->prEnter(compact('control', 'transaction', 'entries', 'split'));
// Verify that we have a transaction
if (empty($transaction['id']))
return $this->prReturn(array('error' => true));
// If the entries are not already split, do so now.
if ($split) {
$result = $this->_splitEntries($control, $transaction, $entries);
if (!empty($result['error']))
return $this->prReturn(array('error' => true));
// Make use of the work done by splitEntries
$transaction = $this->filter_null($transaction) + $result['transaction'];
$entries = $result['entries'];
extract($result['vars']);
/* // Verify the entries */
/* $ret = array(); */
/* if (!$this->verifyTransaction($transaction, $entries)) */
/* return $this->prReturn(array('error' => true) + $ret); */
}
$this->id = $transaction['id'];
$transaction['stamp'] = $this->field('stamp');
$transaction['customer_id'] = $this->field('customer_id');
// Set up our return array
$ret = array();
$ret['entries'] = array();
$ret['error'] = false;
// Go through the entries
foreach ($entries AS $e_index => &$entry) {
// Ensure these items are null'ed out so we don't
// accidentally pick up stale data.
$le1 = $le1_tender = $le2 = $se = null;
extract($entry);
if (!empty($le1) && !empty($le2)) {
$le1['transaction_id'] = $le2['transaction_id'] = $transaction['id'];
if (isset($le1_tender))
$le1_tender['customer_id'] = $transaction['customer_id'];
$result = $this->LedgerEntry->DoubleEntry->addDoubleEntry($le1, $le2, $le1_tender);
$ret['entries'][$e_index]['DoubleEntry'] = $result;
if ($result['error']) {
$ret['error'] = true;
continue;
}
}
if (!empty($se)) {
$se['transaction_id'] = $transaction['id'];
if (empty($se['effective_date']))
$se['effective_date'] = $transaction['stamp'];
$result = $this->StatementEntry->addStatementEntry($se);
$ret['entries'][$e_index]['StatementEntry'] = $result;
if ($result['error']) {
$ret['error'] = true;
continue;
}
}
}
return $this->prReturn($ret);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: _splitEntries
* - An internal helper function capable of splitting an array of
* combined ledger/statement entries into their indivdual entry
* components (2 ledger entries, and a statement entry), based
* on the typical requirements. Any custom split will have to
* be done outside of this function.
*/
function _splitEntries($control, $transaction, $entries) {
$this->prEnter(compact('control', 'transaction', 'entries'));
// Verify that we have a transaction and entries
if (empty($transaction) ||
($transaction['type'] !== 'CLOSE' && empty($entries)))
@@ -523,8 +671,18 @@ class Transaction extends AppModel {
$transaction['customer_id'] = $L->field('customer_id');
}
if (!empty($transaction['account_id'])) {
if (empty($transaction['ledger_id']))
$transaction['ledger_id'] =
$this->Account->currentLedgerID($transaction['account_id']);
if (empty($transaction['crdr']))
$transaction['crdr'] = strtoupper($this->Account->fundamentalType
($transaction['account_id']));
}
// Some transactions do not have their statement entries
// generated from this function, but they are instead
// generated directly as part of the transaction, but are
// created in the final steps during the reconciliation
// phase by the assignCredits function. Keep track of
// what type the statement entries _would_ have been, so
@@ -534,7 +692,7 @@ class Transaction extends AppModel {
// Break each entry out of the combined statement/ledger entry
// and into individual entries appropriate for saving. While
// we're at it, calculate the transaction total as well.
$transaction_amount = 0;
$transaction['amount'] = 0;
foreach ($entries AS &$entry) {
// Ensure these items are null'ed out so we don't
// accidentally pick up stale data.
@@ -558,6 +716,10 @@ class Transaction extends AppModel {
$entry['statement_entry_comment'] = null;
}
if (empty($entry['crdr']) && !empty($transaction['crdr']))
$entry['crdr'] = strtoupper($this->Account->fundamentalOpposite
($transaction['crdr']));
// Priority goes to settings defined in $entry, but
// use the control information as defaults.
$entry += $control;
@@ -576,7 +738,7 @@ class Transaction extends AppModel {
array_intersect_key($entry,
array_flip(array('account_id', 'amount')));
$le2['ledger_id'] = $entry['new_ledger_id'];
$le2['crdr'] = strtoupper($this->Account->fundamentalOpposite($le1['crdr']));
$le2['crdr'] = strtoupper($this->Account->fundamentalType($le2['account_id']));
$le2['comment'] = "Ledger Balance Forward (b/f)";
}
else {
@@ -586,6 +748,15 @@ class Transaction extends AppModel {
array_intersect_key($transaction,
array_flip(array('ledger_id', 'account_id', 'crdr')));
}
if ($entry['amount'] < 0 && !empty($entry['force_positive'])) {
$le1['amount'] *= -1;
$le2['amount'] *= -1;
$entry += array('swap_crdr' => true);
}
if (!empty($entry['swap_crdr']))
list($le1['crdr'], $le2['crdr']) = array($le2['crdr'], $le1['crdr']);
}
else
$le1 = $le1_tender = $le2 = null;
@@ -620,84 +791,15 @@ class Transaction extends AppModel {
}
// Add entry amount into the transaction total
$transaction_amount += $entry['amount'];
$transaction['amount'] += $entry['amount'];
// Replace combined entry with our new individual entries
$entry = compact('le1', 'le1_tender', 'le2', 'se');
}
// If transaction amount is not already set, use the
// sum of the entry amounts.
$transaction += array('amount' => $transaction_amount);
$this->pr(20, compact('transaction', 'entries'));
// Move forward, verifying and saving everything.
$ret = array();
if (!$this->verifyTransaction($transaction, $entries))
return $this->prReturn(array('error' => true) + $ret);
// Save transaction to the database
$this->create();
if (!$this->save($transaction))
return $this->prReturn(array('error' => true) + $ret);
$transaction_stamp = $this->field('stamp');
// Set up our return ids array
$ret['transaction_id'] = $this->id;
$ret['entries'] = array();
$ret['error'] = false;
// Go through the entries
foreach ($entries AS $e_index => &$entry) {
// Ensure these items are null'ed out so we don't
// accidentally pick up stale data.
$le1 = $le1_tender = $le2 = $se = null;
extract($entry);
if (!empty($le1) && !empty($le2)) {
$le1['transaction_id'] = $le2['transaction_id'] = $ret['transaction_id'];
if (isset($le1_tender))
$le1_tender['customer_id'] = $transaction['customer_id'];
$result = $this->LedgerEntry->DoubleEntry->addDoubleEntry($le1, $le2, $le1_tender);
$ret['entries'][$e_index]['DoubleEntry'] = $result;
if ($result['error']) {
$ret['error'] = true;
continue;
}
}
if (!empty($se)) {
$se['transaction_id'] = $ret['transaction_id'];
if (empty($se['effective_date']))
$se['effective_date'] = $transaction_stamp;
$result = $this->StatementEntry->addStatementEntry($se);
$ret['entries'][$e_index]['StatementEntry'] = $result;
if ($result['error']) {
$ret['error'] = true;
continue;
}
}
}
if (!empty($control['assign']) && !$ret['error']) {
$result = $this->StatementEntry->assignCredits
(null,
(empty($control['assign_receipt']) ? null
: $ret['transaction_id']),
null,
$assign_disbursement_type,
$transaction['customer_id'],
$transaction['lease_id']
);
$ret['assigned'] = $result;
if ($result['error'])
$ret['error'] = true;
}
$this->Customer->update($transaction['customer_id']);
return $this->prReturn($ret);
return $this->prReturn(compact('transaction', 'entries')
+ array('vars' => compact('assign_disbursement_type'))
+ array('error' => false));
}
@@ -726,7 +828,6 @@ class Transaction extends AppModel {
'Entry' =>
array(array('tender_id' => null,
'account_id' => $this->Account->nsfAccountID(),
'crdr' => 'DEBIT',
'amount' => $tender['LedgerEntry']['amount'],
))),
$tender['Transaction']['account_id']);
@@ -807,7 +908,6 @@ class Transaction extends AppModel {
'include_statement_entry' => false,
'amount' => $rollback['Transaction']['amount'],
'account_id' => $this->Account->accountReceivableAccountID(),
'crdr' => 'DEBIT',
);
// Set the transaction amount to be negative
@@ -848,9 +948,28 @@ class Transaction extends AppModel {
if ($charge_result['error'])
return $this->prReturn(array('error' => true) + $ret);
if (!empty($ret['rollback'])) {
foreach ($ret['rollback']['entries'] AS $rentry) {
if (!empty($rentry['DoubleEntry'])) {
if (!empty($rentry['DoubleEntry']['error']))
continue;
foreach (array('Entry1', 'Entry2') AS $n) {
$entry = $rentry['DoubleEntry'][$n];
if ($entry['data']['account_id'] == $this->Account->nsfAccountID()) {
if (!empty($ret['nsf_ledger_entry_id']))
$this->INTERNAL_ERROR("More than one NSF LE ID");
$ret['nsf_ledger_entry_id'] = $entry['ledger_entry_id'];
}
}
}
}
}
if (empty($ret['rollback']['error']) && empty($ret['nsf_ledger_entry_id']))
$this->INTERNAL_ERROR("NSF LE ID not found under rollback entries");
$ret['nsf_transaction_id'] = $ret['bounce']['transaction_id'];
if (!empty($ret['rollback']))
$ret['nsf_ledger_entry_id'] = $ret['rollback']['entries'][0]['DoubleEntry']['Entry1']['ledger_entry_id'];
return $this->prReturn($ret + array('error' => false));
}
@@ -885,18 +1004,16 @@ class Transaction extends AppModel {
// These are all disbursements against the charge we're reversing
$rollback =
array('control' =>
array('assign' => true,
'assign_receipt' => true,
'include_ledger_entry' => false,
array('include_ledger_entry' => false,
'include_statement_entry' => true,
),
'Transaction' =>
array('stamp' => $stamp,
'type' => 'CREDIT_NOTE',
'crdr' => 'DEBIT',
'crdr' => 'CREDIT',
'account_id' => $this->Account->accountReceivableAccountID(),
'amount' => $charge['StatementEntry']['amount'],
'account_id' => $charge['StatementEntry']['account_id'],
'customer_id' => $charge['StatementEntry']['customer_id'],
'lease_id' => null,
'comment' => $comment,
@@ -904,32 +1021,49 @@ class Transaction extends AppModel {
'Entry' => array());
foreach ($disb_entries['DisbursementEntry'] AS $disbursement) {
$rollback['Entry'][] =
array(
'type' => $disbursement['type'],
//'type' => 'REVERSAL',
'amount' => -1 * $disbursement['amount'],
'account_id' => $disbursement['account_id'],
'customer_id' => $disbursement['customer_id'],
'lease_id' => $disbursement['lease_id'],
'charge_entry_id' => $disbursement['charge_entry_id'],
);
}
// Add the sole ledger entry for this transaction
// Reverse the charge
$rollback['Entry'][] =
array('include_ledger_entry' => true,
'include_statement_entry' => true,
'type' => 'REVERSAL',
'crdr' => 'CREDIT',
'amount' => $rollback['Transaction']['amount'],
'account_id' => $this->Account->accountReceivableAccountID(),
'account_id' => $charge['StatementEntry']['account_id'],
'amount' => $charge['StatementEntry']['amount'],
'customer_id' => $charge['StatementEntry']['customer_id'],
'lease_id' => $charge['StatementEntry']['lease_id'],
'charge_entry_id' => $charge['StatementEntry']['id'],
);
$customer_credit = 0;
foreach ($disb_entries['DisbursementEntry'] AS $disbursement) {
$rollback['Entry'][] =
array(
'include_ledger_entry' =>
($disbursement['type'] !== 'DISBURSEMENT'),
'force_positive' => true,
'type' => $disbursement['type'],
'amount' => -1 * $disbursement['amount'],
'account_id' =>
($disbursement['type'] === 'DISBURSEMENT'
? $this->Account->customerCreditAccountID()
: $disbursement['account_id']),
'customer_id' => $disbursement['customer_id'],
'lease_id' => $disbursement['lease_id'],
'charge_entry_id' => $disbursement['charge_entry_id'],
);
if ($disbursement['type'] === 'DISBURSEMENT')
$customer_credit += $disbursement['amount'];
}
// Create an explicit surplus entry for the customer credit.
// Do it BEFORE assigning credits to outstanding charges to
// ensure that those charges are paid from the customer surplus
// account thus and we don't end up with bizarre disbursements,
// like having Rent paid from Damage (or whatever account the
// reversed charge came from).
$rollback['control']['assign'] = array($customer_credit, 'assign');
// Record the transaction, which will un-disburse previously
// disbursed payments, and other similar work.
if (count($rollback['Entry'])) {
@@ -1030,7 +1164,17 @@ class Transaction extends AppModel {
if (empty($query['fields']))
$query['fields'] = array();
$stats = array();
// Get the overall total
$squery = $query;
$squery['fields'][] = "SUM(Transaction.amount) AS total";
$squery['fields'][] = "COUNT(Transaction.id) AS count";
$stats = $this->find('first', $squery);
if (empty($stats))
return $this->prReturn(array());
$stats = $stats[0];
unset($stats[0]);
foreach ($this->hasMany AS $table => $association) {
// Only calculate stats for *Entry types
if (!preg_match("/Entry$/", $table) &&
@@ -1069,6 +1213,11 @@ class Transaction extends AppModel {
unset($stats[$table][0]);
}
// Add summary data, which may or may not be useful
// or even meaningful, depending on what the caller
// has queried (it's up to them to make that decision).
$stats['undisbursed'] = $stats['total'] - $stats['StatementEntry']['disbursements'];
return $this->prReturn($stats);
}
}