Another snapshot. I think I'll be taking the CREDIT/DEBIT reconcile functionality out.

git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716/site@353 97e9348a-65ac-dc4b-aefc-98561f571b83
This commit is contained in:
abijah
2009-07-20 02:17:54 +00:00
parent 3d262fd4db
commit 7304a0e889
6 changed files with 266 additions and 244 deletions

View File

@@ -270,7 +270,7 @@ class AccountsController extends AppController {
}
function tst($id) {
$entries = $this->Account->ledgerEntries($id, true);
$entries = $this->Account->unreconciledEntries($id);
pr($entries);
}
}

View File

@@ -456,15 +456,15 @@ class CustomersController extends AppController {
$this->layout = null;
$this->autoLayout = false;
$this->autoRender = false;
Configure::write('debug', '0');
header("Content-type: text/xml;charset=utf-8");
//Configure::write('debug', '0');
//header("Content-type: text/xml;charset=utf-8");
App::import('Helper', 'Xml');
$xml = new XmlHelper();
// Find the unreconciled entries, then manipulate the structure
// slightly to accomodate the format necessary for XML Helper.
$unreconciled = $this->Customer->findUnreconciledLedgerEntries($id);
$unreconciled = $this->Customer->unreconciledCharges($id);
$unreconciled = array('entries' =>
array_intersect_key($unreconciled['debit'],
array('entry'=>1, 'balance'=>1)));

View File

@@ -325,83 +325,8 @@ class Account extends AppModel {
* account, either just from the current ledger, or from all ledgers.
*/
function ledgerEntries($id, $all = false, $cond = null, $link = null) {
/* pr(array('function' => 'Account::findLedgerEntries', */
/* 'args' => compact('id', 'all', 'cond', 'link'), */
/* )); */
/* $this->Entry->find */
/* ('all', array */
/* ('contain' => array(), */
/* 'conditions' => array('Entry.account_id' => $id) */
/* )); */
$ledgers = $this->ledgers($id, $all);
/* $this->Ledger->DoubleEntry->find */
/* ('all', array */
/* ('contain' => array('Ledger'), */
/* 'conditions' => array('OR' => */
/* array('DoubleEntry.debit_ledger_id' => $ledgers), */
/* array('DoubleEntry.credit_ledger_id' => $ledgers)), */
/* 'fields' => */
/* )); */
$entries = $this->Ledger->find
('all', array
('link' =>
array('Account',
'DoubleEntry' => array
('fields' => $this->Ledger->DoubleEntry->debitCreditFields('DoubleEntry', 'Ledger', false)),
),
'conditions' => array('Ledger.id' => $ledgers),
));
/* $stats = $this->stats($id, $all, $cond); */
/* $entries = array('Entries' => $entries, */
/* 'summary' => $stats['Ledger']); */
/* pr(array('function' => 'Account::findLedgerEntries', */
/* 'args' => compact('id', 'all', 'cond', 'link'), */
/* 'vars' => compact('stats'), */
/* 'return' => compact('entries'), */
/* )); */
return $entries;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: findLedgerEntriesRelatedToAccount
* - Returns an array of ledger entries that belong to the given
* account, and are related to a specific account, either just from
* the current ledger, or from all ledgers.
*/
function findLedgerEntriesRelatedToAccount($id, $rel_ids, $all = false, $cond = null, $link = null) {
/* pr(array('function' => 'Account::findLedgerEntriesRelatedToAccount', */
/* 'args' => compact('id', 'rel_ids', 'all', 'cond', 'link'), */
/* )); */
if (!isset($cond))
$cond = array();
if (!is_array($rel_ids))
$rel_ids = array($rel_ids);
$ledger_ids = array();
foreach ($rel_ids AS $rel_id)
$ledger_ids = array_merge($ledger_ids, $this->ledgers($rel_id));
array_push($cond, $this->Ledger->DoubleEntry->conditionEntryAsCreditOrDebit($ledger_ids));
$entries = $this->findLedgerEntries($id, $all, $cond, $link);
/* pr(array('function' => 'Account::findLedgerEntriesRelatedToAccount', */
/* 'args' => compact('id', 'relid', 'all', 'cond', 'link'), */
/* 'vars' => compact('ledger_ids'), */
/* 'return' => compact('entries'), */
/* )); */
return $entries;
return $this->Ledger->ledgerEntries($ledgers, $cond, $link);
}
@@ -413,56 +338,23 @@ class Account extends AppModel {
* (such as charges not paid).
*/
function findUnreconciledLedgerEntries($id = null, $fundamental_type = null, $cond = null) {
function unreconciledEntries($id, $set, $cond = null, $link = null) {
if (!isset($cond))
$cond = array();
if (!isset($link))
$link = array();
$link['Account'] = array('fields' => array());
$cond[] = array('Account.id' => $id);
foreach (($fundamental_type
? array($fundamental_type)
: array('debit', 'credit')) AS $fund) {
$ucfund = ucfirst($fund);
$unreconciled[$fund]['entry'] = $this->find
('all', array
('link' => array
('Ledger' => array
('fields' => array(),
"DoubleEntry" => array
('class' => "{$ucfund}DoubleEntry",
'fields' => array('id', 'customer_id', 'lease_id', 'amount'),
"ReconciliationDoubleEntry" => array
('class' => "{$ucfund}ReconciliationDoubleEntry",
'fields' => array
("COALESCE(SUM(Reconciliation.amount),0) AS 'reconciled'",
"DoubleEntry.amount - COALESCE(SUM(Reconciliation.amount),0) AS 'balance'",
),
),
),
),
),
'group' => ("DoubleEntry.id" .
" HAVING DoubleEntry.amount" .
" <> COALESCE(SUM(Reconciliation.amount),0)"),
'conditions' => $cond,
'fields' => array(),
));
$balance = 0;
foreach ($unreconciled[$fund]['entry'] AS &$entry) {
$entry = array_merge(array_diff_key($entry["DoubleEntry"], array(0=>true)),
$entry[0]);
$balance += $entry['balance'];
}
$unreconciled[$fund]['balance'] = $balance;
}
return $unreconciled;
$set = $this->Ledger->Entry->reconciledSet($set, $cond, $link, true);
pr(compact('set'));
return $set;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: amountWouldReconcile
* function: paymentWouldReconcile
* - Returns which ledger entries a new credit/debit would
* reconcile, and how much.
*
@@ -473,38 +365,37 @@ class Account extends AppModel {
* whatever algorithm is simplest.
*/
function amountWouldReconcile($id, $fundamental_type, $amount, $cond = null) {
$ofund = $this->fundamentalOpposite($fundamental_type);
function paymentWouldReconcile($id, $amount, $cond = null, $link = null) {
$unreconciled = array($ofund => array('entry'=>array(), 'balance'=>0));
$applied = 0;
// if there is no money in the entry, it can reconcile nothing
// don't bother wasting time sifting ledger entries.
if ($amount > 0) {
$unreconciled = $this->findUnreconciledLedgerEntries($id, $ofund, $cond);
if ($amount <= 0)
return;
foreach ($unreconciled[$ofund]['entry'] AS $i => &$entry) {
// Determine if amount is sufficient to cover the entry
if ($amount > $entry['balance'])
$apply = $entry['balance'];
elseif ($amount > 0)
$apply = $amount;
else {
unset($unreconciled[$ofund]['entry'][$i]);
continue;
}
$unreconciled = $this->unreconciledEntries($id, 'CHARGE', $cond, $link);
$entry['applied'] = $apply;
$entry['reconciled'] += $apply;
$entry['balance'] -= $apply;
$applied += $apply;
$amount -= $apply;
foreach ($unreconciled AS $i => &$item) {
$entry =& $item['DoubleEntry'];
// Determine if amount is sufficient to cover the entry
if ($amount > $entry['balance'])
$apply = $entry['balance'];
elseif ($amount > 0)
$apply = $amount;
else {
unset($unreconciled[$i]);
continue;
}
$entry['applied'] = $apply;
$entry['reconciled'] += $apply;
$entry['balance'] -= $apply;
$applied += $apply;
$amount -= $apply;
}
$unreconciled[$ofund]['unapplied'] = $amount;
$unreconciled[$ofund]['applied'] = $applied;
$unreconciled[$ofund]['balance'] -= $applied;
$unreconciled['unapplied'] = $amount;
$unreconciled['applied'] = $applied;
$unreconciled['balance'] -= $applied;
return $unreconciled;
}

View File

@@ -82,7 +82,7 @@ class Customer extends AppModel {
/* )); */
$A = new Account();
$entries = $A->findLedgerEntries
$entries = $A->ledgerEntries
($A->securityDepositAccountID(),
true, array('DoubleEntry.customer_id' => $id), $link);
@@ -96,6 +96,80 @@ class Customer extends AppModel {
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: unreconciledCharges
* - Returns charges have not yet been fully paid
*/
function unreconciledCharges($id, $cond = null, $link = null) {
if (!isset($cond))
$cond = array();
if (!isset($link))
$link = array();
if (!isset($link['DoubleEntry']))
$link['DoubleEntry'] = array();
if (!isset($link['DoubleEntry']['Customer']))
$link['DoubleEntry']['Customer'] = array();
if (!isset($link['DoubleEntry']['Customer']['fields']))
$link['DoubleEntry']['Customer']['fields'] = array();
$cond[] = array('Customer.id' => $id);
$set = $this->DoubleEntry->Entry->reconciledSet('CHARGE', $cond, $link, true);
pr(compact('set'));
return $set;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: paymentWouldReconcile
* - Returns which ledger entries a new credit/debit would
* reconcile, and how much.
*
* - REVISIT <AP> 20090617
* This should be subject to different algorithms, such
* as apply to oldest charges first, newest first, to fees
* before rent, etc. Until we get there, I'll hardcode
* whatever algorithm is simplest.
*/
function paymentWouldReconcile($id, $amount, $cond = null, $link = null) {
$unreconciled = array($ofund => array('entry'=>array(), 'balance'=>0));
$applied = 0;
if ($amount <= 0)
return;
$unreconciled = $this->unreconciledEntries($id, 'CHARGE', $cond, $link);
foreach ($unreconciled AS $i => &$item) {
$entry =& $item['DoubleEntry'];
// Determine if amount is sufficient to cover the entry
if ($amount > $entry['balance'])
$apply = $entry['balance'];
elseif ($amount > 0)
$apply = $amount;
else {
unset($unreconciled[$i]);
continue;
}
$entry['applied'] = $apply;
$entry['reconciled'] += $apply;
$entry['balance'] -= $apply;
$applied += $apply;
$amount -= $apply;
}
$unreconciled['unapplied'] = $amount;
$unreconciled['applied'] = $applied;
$unreconciled['balance'] -= $applied;
return $unreconciled;
}
/**************************************************************************
**************************************************************************
**************************************************************************

View File

@@ -373,19 +373,95 @@ OPTION 2
/**************************************************************************
**************************************************************************
**************************************************************************
* function: reconcilingEntries
* function: reconciledSet
* - Returns the set of entries satisfying the given conditions,
* along with any entries that they reconcile
*/
function reconciledSet($set, $cond = null, $link = null, $unrec = false) {
if (!isset($cond))
$cond = array();
if (!isset($link))
$link = array();
if ($set == 'CHARGE' || $set == 'PAYMENT')
$cond[] = array('Entry.type' => $set);
elseif ($set == 'DEBIT' || $set == 'CREDIT')
$cond[] = array('Entry.crdr' => $set);
else
die("INVALID RECONCILE SET");
$link['DoubleEntry'] += array();
/* if ($set == 'CHARGE') */
/* $link['Payment'] = array('fields' => array("SUM(ChargesPayment.amount) AS applied")); */
/* if ($set == 'PAYMENT') */
/* $link['Charge'] = array('fields' => array("SUM(ChargesPayment.amount) AS applied")); */
/* if ($set == 'DEBIT') */
/* $link['Credit'] = array('fields' => array("SUM(Reconciliation.amount) AS applied")); */
/* if ($set == 'CREDIT') */
/* $link['Debit'] = array('fields' => array("SUM(Reconciliation.amount) AS applied")); */
if ($set == 'CHARGE')
$link['Payment'] = array('fields' => array("SUM(AppliedPayment.amount) AS applied"));
if ($set == 'PAYMENT')
$link['Charge'] = array('fields' => array("SUM(AppliedCharge.amount) AS applied"));
if ($set == 'DEBIT')
$link['Credit'] = array('fields' => array("SUM(AppliedCredit.amount) AS applied"));
if ($set == 'CREDIT')
$link['Debit'] = array('fields' => array("SUM(AppliedDebit.amount) AS applied"));
$result = $this->find
('all', array
(
'link' => $link,
'conditions' => $cond,
'group' => 'Entry.id',
));
pr(array('reconciledSet', compact('set', 'cond', 'link', 'result')));
$resultset = array();
foreach ($result AS $i => $entry) {
$entry['DoubleEntry'] += $entry[0];
unset($entry[0]);
$entry['DoubleEntry']['balance'] =
$entry['DoubleEntry']['amount'] - $entry['DoubleEntry']['applied'];
// Since HAVING isn't a builtin feature of CakePHP,
// we'll have to post-process to get the desired entries
if ($entry['DoubleEntry']['balance'] == 0) {
if (!$unrec)
$resultset[] = $entry;
}
else {
if ($unrec)
$resultset[] = $entry;
}
}
//$result['stats'] = $this->stats(null, $cond);
pr($this->stats(null, $cond, $link));
return $resultset;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: reconciledEntries
* - Returns a list of entries that reconcile against the given entry.
* (such as payments towards a charge).
*/
function reconciledEntries($id, $cond = null) {
function reconciledEntries($id, $cond = null, $link = null) {
if (!isset($cond))
$cond = array();
if (!isset($link))
$link = array();
$cond[] = array('Entry.id' => $id);
$entry = $this->find('first', array('recursive' => -1));
$entry = $this->find('first', array('conditions' => array('Entry.id' => $id),
'recursive' => -1));
$entry = $entry['Entry'];
$contain = array();
@@ -471,42 +547,49 @@ OPTION 2
* function: stats
* - Returns summary data from the requested ledger entry
*/
function stats($id, $cond = null) {
function stats($id = null, $cond = null, $link = null) {
if (!isset($cond))
$cond = array();
if (!isset($link))
$link = array();
$cond[] = array('Entry.id' => $id);
if (isset($id))
$cond[] = array('Entry.id' => $id);
$entry = $this->find('first', array('contain' => array('DoubleEntry.amount'),
'fields' => array('Entry.type', 'Entry.crdr')));
$entry['Entry'] += $entry['DoubleEntry'];
$entry = $entry['Entry'];
/* $entry = $this->find('first', array('contain' => array('DoubleEntry.amount'), */
/* 'fields' => array('Entry.type', 'Entry.crdr'), */
/* 'conditions' => array('Entry.id' => $id))); */
/* pr(compact('entry')); */
/* $entry['Entry'] += $entry['DoubleEntry']; */
/* $entry = $entry['Entry']; */
$stats = array();
foreach(array('charge', 'payment', 'debit', 'credit') AS $rtype) {
$Rtype = ucfirst($rtype);
if (($rtype == 'charge' && $entry['type'] == 'PAYMENT') ||
$rlink = $link;
$rlink[$Rtype] = array('fields' => array("SUM(Applied{$Rtype}.amount) AS applied"));
pr(array('stats()', compact('id', 'cond', 'link', 'rlink')));
if (1 || ($rtype == 'charge' && $entry['type'] == 'PAYMENT') ||
($rtype == 'payment' && $entry['type'] == 'CHARGE') ||
($rtype == 'debit' && $entry['crdr'] == 'CREDIT') ||
($rtype == 'credit' && $entry['crdr'] == 'DEBIT')) {
$result = $this->find
('first', array
('link' =>
array($Rtype =>
array('fields' => array("SUM(Applied{$Rtype}.amount) AS applied"))),
'fields' => array(),
('link' => $rlink,
'fields' => array("SUM(DoubleEntry.amount) AS total",
"SUM(DoubleEntry.amount) - SUM(Applied{$Rtype}.amount) AS unapplied"),
'conditions' => $cond,
'group' => 'Entry.id',
//'group' => 'Entry.id',
));
//pr(compact('Rtype', 'result'));
pr(compact('Rtype', 'result'));
$sumfld = $Rtype;
$stats[$sumfld] = $result[0];
if (!isset($stats[$sumfld]['applied']))
$stats[$sumfld]['applied'] = 0;
$stats[$sumfld]['unapplied'] = $entry['amount'] - $stats[$sumfld]['applied'];
/* if (!isset($stats[$sumfld]['applied'])) */
/* $stats[$sumfld]['applied'] = 0; */
}
}

View File

@@ -8,6 +8,8 @@ class Ledger extends AppModel {
);
var $hasMany = array(
'Entry',
'DoubleEntry' => array(
'foreignKey' => false,
@@ -27,17 +29,6 @@ class Ledger extends AppModel {
'counterQuery' => ''
),
'DebitLedgerEntry' => array(
'className' => 'DoubleEntry',
'foreignKey' => 'debit_ledger_id',
'dependent' => false,
),
'CreditLedgerEntry' => array(
'className' => 'DoubleEntry',
'foreignKey' => 'credit_ledger_id',
'dependent' => false,
),
);
@@ -129,66 +120,58 @@ class Ledger extends AppModel {
}
function debitCreditFields($id, $sum = false, $entry_name = 'Entry', $double_name = 'DoubleEntry') {
$ftype = strtoupper($this->Account->fundamentalType($this->accountID($id)));
$fields = array
(
($sum ? 'SUM(' : '') .
"IF({$entry_name}.crdr = 'DEBIT', {$double_name}.amount, NULL)" .
($sum ? ')' : '') . ' AS debit' . ($sum ? 's' : ''),
($sum ? 'SUM(' : '') .
"IF({$entry_name}.crdr = 'CREDIT', {$double_name}.amount, NULL)" .
($sum ? ')' : '') . ' AS credit' . ($sum ? 's' : ''),
($sum ? 'SUM(' : '') .
"IF({$entry_name}.crdr = '$ftype', 1, -1) * {$double_name}.amount" .
// IF({$double_name}.amount, {$double_name}.amount, 0)" .
($sum ? ')' : '') . ' AS balance',
);
if ($sum)
$fields[] = "COUNT({$entry_name}.id) AS entries";
return $fields;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: ledgerEntries
* - Returns an array of ledger entries that belong to a given
* ledger. There is extra work done... see the DoubleEntry model.
* ledger. There is extra work done to establish debit/credit
*/
function ledgerEntries($id, $account_type = null, $cond = null, $link = null) {
/* pr(array('function' => 'Ledger::findLedgerEntries', */
/* 'args' => compact('id', 'account_type', 'cond', 'link'), */
/* )); */
function ledgerEntries($ids, $cond = null, $link = null) {
if (empty($ids))
return null;
if (!isset($account_type)) {
$ledger = $this->find('first', array
('contain' => array
('Account' => array
('fields' => array('type'),
),
),
'fields' => array(),
'conditions' => array(array('Ledger.id' => $id)),
));
$account_type = $ledger['Account']['type'];
}
/* $id = (is_array($ids) ? $ids[0] : $ids); */
/* $ftype = strtoupper($this->Account->fundamentalType($this->accountID($id))); */
// If the requested entries are limited by date, we must calculate
// a balance forward, or the resulting balance will be thrown off.
//
// REVISIT <AP>: This obviously is more general than date.
// As such, it will not work (or, only work if the
// condition only manages to exclude the first parts
// of the ledger, nothing in the middle or at the
// end. For now, I'll just create an 'other' entry,
// not necessarily a balance forward.
$entries = $this->Entry->find
('all', array
('contain' => array('DoubleEntry' => array('fields' => array('amount'))),
'fields' => array_merge(array("Entry.*"),
$this->debitCreditFields(is_array($ids) ? $ids[0] : $ids)),
/* "IF(Entry.crdr = 'CREDIT', DoubleEntry.amount, NULL) AS credit", */
/* "IF(Entry.crdr = 'DEBIT', DoubleEntry.amount, NULL) AS debit", */
/* "IF(Entry.crdr = '$ftype', 1, -1) * DoubleEntry.amount AS balance", */
$bf = array();
if (0 && isset($cond)) {
//$date = '<NOT IMPLEMENTED>';
$stats = $this->stats($id, array('NOT' => array($cond)));
$bf = array(array(array('debit' => $stats['debits'],
'credit' => $stats['credits'],
'balance' => $stats['balance']),
'conditions' => array('Entry.ledger_id' => $ids),
));
'DoubleEntry' => array('id' => null,
//'comment' => "Balance Forward from $date"),
'comment' => "-- SUMMARY OF EXCLUDED ENTRIES --"),
'Transaction' => array('id' => null,
//'stamp' => $date,
'stamp' => null,
'comment' => null),
));
}
$entries = $this->DoubleEntry->findInLedgerContext($id, $account_type, $cond, $link);
/* pr(array('function' => 'Ledger::findLedgerEntries', */
/* 'args' => compact('id', 'account_type', 'cond', 'link'), */
/* 'vars' => compact('ledger'), */
/* 'return' => compact('entries'), */
/* )); */
pr(compact('entries'));
return $entries;
}
@@ -209,23 +192,14 @@ class Ledger extends AppModel {
('link' =>
array(// Models
'Account' => array('fields' => array()),
//'DoubleEntry' => array('fields' => array()),
'DoubleEntry' =>
array('fields' => array(),
'Transaction' => array('fields' => array('stamp')),
),
'Entry' => array
('DoubleEntry' =>
array('fields' => array(),
'Transaction' => array('fields' => array('stamp')),
),
),
),
'fields' =>
array("SUM(IF(DoubleEntry.debit_ledger_id = Ledger.id,
DoubleEntry.amount, NULL)) AS debits",
"SUM(IF(DoubleEntry.credit_ledger_id = Ledger.id,
DoubleEntry.amount, NULL)) AS credits",
"SUM(IF(Account.type IN ('ASSET', 'EXPENSE'),
IF(DoubleEntry.debit_ledger_id = Ledger.id, 1, -1),
IF(DoubleEntry.credit_ledger_id = Ledger.id, 1, -1)
) * IF(DoubleEntry.amount, DoubleEntry.amount, 0)
) AS balance",
"COUNT(DoubleEntry.id) AS entries"),
'fields' => $this->debitCreditFields($id, true),
'conditions' => $cond,
'group' => 'Ledger.id',
));