Files
pmgr/site/models/account.php

852 lines
30 KiB
PHP

<?php
class Account extends AppModel {
var $hasOne = array(
'CurrentLedger' => array(
'className' => 'Ledger',
// REVISIT <AP> 20090702:
// I would prefer this statement, which has no
// engine specific code. However, it doesn't
// work with the Linkable behavior. I need to
// look into that, just not right now.
//'conditions' => array('CurrentLedger.close_id' => null),
'conditions' => array('CurrentLedger.close_id IS NULL'),
),
);
var $hasMany = array(
'Ledger',
'LedgerEntry',
);
/**************************************************************************
**************************************************************************
**************************************************************************
* function: type
* - Returns the type of this account
*/
function type($id) {
$this->cacheQueries = true;
$account = $this->find('first', array
('recursive' => -1,
'fields' => array('type'),
'conditions' => array(array('Account.id' => $id)),
));
$this->cacheQueries = false;
return $account['Account']['type'];
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: fundamentalType
* - Returns the fundmental type of the account, credit or debit
*/
function fundamentalType($id_or_type) {
if (is_numeric($id_or_type))
$type = $this->type($id_or_type);
else
$type = $id_or_type;
// Asset and Expense accounts are debit accounts
if (in_array(strtoupper($type), array('ASSET', 'EXPENSE')))
return 'debit';
// Otherwise, it's a credit account
return 'credit';
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: fundamentalOpposite
* - Returns the opposite fundmental type of the account, credit or debit
*/
function fundamentalOpposite($id_or_type) {
if (in_array(strtolower($id_or_type), array('credit', 'debit')))
$fund = $id_or_type;
else
$fund = $this->fundamentalType($id_or_type);
if ($fund == 'debit')
return 'credit';
return 'debit';
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: name
* - Returns the name of this account
*/
function name($id) {
$this->cacheQueries = true;
$account = $this->find('first', array
('recursive' => -1,
'fields' => array('name'),
'conditions' => array(array('Account.id' => $id)),
));
$this->cacheQueries = false;
return $account['Account']['name'];
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: debitCreditFields
* - Returns the fields necessary to determine whether the queried
* entries are a debit, or a credit, and also the effect each have
* on the overall balance of the account.
*/
function debitCreditFields($sum = false, $entry_name = 'LedgerEntry', $account_name = 'Account') {
return $this->LedgerEntry->debitCreditFields
($sum, $entry_name, $account_name);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: Account IDs
* - Returns the ID of the desired account
*/
function securityDepositAccountID() { return $this->nameToID('Security Deposit'); }
function rentAccountID() { return $this->nameToID('Rent'); }
function lateChargeAccountID() { return $this->nameToID('Late Charge'); }
function nsfAccountID() { return $this->nameToID('NSF'); }
function nsfChargeAccountID() { return $this->nameToID('NSF Charge'); }
function taxAccountID() { return $this->nameToID('Tax'); }
function accountReceivableAccountID() { return $this->nameToID('A/R'); }
function cashAccountID() { return $this->nameToID('Cash'); }
function checkAccountID() { return $this->nameToID('Check'); }
function moneyOrderAccountID() { return $this->nameToID('Money Order'); }
function concessionAccountID() { return $this->nameToID('Concession'); }
function pettyCashAccountID() { return $this->nameToID('Petty Cash'); }
function invoiceAccountID() { return $this->nameToID('Invoice'); }
function receiptAccountID() { return $this->nameToID('Receipt'); }
function badDebtAccountID() { return $this->nameToID('Bad Debt'); }
/**************************************************************************
**************************************************************************
**************************************************************************
* function: fundamentalAccounts
* - Returns an array of accounts by their fundamental type
*/
function fundamentalAccounts($ftype) {
$this->cacheQueries = true;
$account = $this->find('all', array
('contain' => array('CurrentLedger'),
'fields' => array('Account.id', 'Account.type', 'Account.name', 'CurrentLedger.id'),
'conditions' => array('Account.type' => strtoupper($ftype))
));
$this->cacheQueries = false;
return $account;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: relatedAccounts
* - Returns an array of accounts related by similar attributes
*/
function relatedAccounts($attribute, $extra = null) {
$this->cacheQueries = true;
$account = $this->find('all', array
('contain' => array('CurrentLedger'),
'fields' => array('Account.id', 'Account.type', 'Account.name', 'CurrentLedger.id'),
'conditions' => array('Account.'.$attribute => true),
'order' => array('Account.name'),
) + (isset($extra) ? $extra : array())
);
$this->cacheQueries = false;
return $account;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: chargeAccounts
* - Returns an array of accounts suitable for charges
*/
function chargeAccounts() {
// Get all accounts that support charges
$accounts = $this->relatedAccounts('chargeable', array('order' => 'name'));
// Rearrange to be of the form (id => name)
$charge_accounts = array();
foreach ($accounts AS $acct) {
$charge_accounts[$acct['Account']['id']] = $acct['Account']['name'];
}
return $charge_accounts;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: paymentAccounts
* - Returns an array of accounts suitable for payments
*/
function paymentAccounts() {
// Get all accounts that support payments
$accounts = $this->relatedAccounts('payable', array('order' => 'name'));
// Rearrange to be of the form (id => name)
$payment_accounts = array();
foreach ($accounts AS $acct) {
$payment_accounts[$acct['Account']['id']] = $acct['Account']['name'];
}
return $payment_accounts;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: collectableAccounts
* - Returns an array of accounts suitable to show income collection
*/
function collectableAccounts() {
$accounts = $this->paymentAccounts();
foreach(array($this->nsfAccountID(),
$this->securityDepositAccountID())
AS $account_id) {
$accounts[$account_id] = $this->name($account_id);
}
return $accounts;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: currentLedgerID
* - Returns the current ledger ID of the account
*/
function currentLedgerID($id) {
$this->cacheQueries = true;
$item = $this->find('first', array
('contain' => 'CurrentLedger',
'conditions' => array('Account.id' => $id),
));
$this->cacheQueries = false;
return $item['CurrentLedger']['id'];
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: ledgers
* - Returns an array of ledger ids from the given account
*/
function ledgers($id, $all = false) {
if ($all) {
$contain = array('Ledger' => array('fields' => array('Ledger.id')));
} else {
$contain = array('CurrentLedger' => array('fields' => array('CurrentLedger.id')));
}
$this->cacheQueries = true;
$account = $this->find('first', array
('contain' => $contain,
'fields' => array(),
'conditions' => array(array('Account.id' => $id)),
));
$this->cacheQueries = false;
if ($all) {
$ledger_ids = array();
foreach ($account['Ledger'] AS $ledger)
array_push($ledger_ids, $ledger['id']);
}
else {
$ledger_ids = array($account['CurrentLedger']['id']);
}
/* pr(array('function' => 'Account::ledgers', */
/* 'args' => compact('id', 'all'), */
/* 'return' => $ledger_ids)); */
return $ledger_ids;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: closeCurrentLedger
* - Closes the current account ledger, and opens a new one
* with the old balance carried forward.
*/
function closeCurrentLedger($id = null, $close_id = null) {
$contain = array('CurrentLedger' => array('fields' => array('CurrentLedger.id')));
if (!$close_id) {
$close = new Close();
$close->create();
if (!$close->save(array('stamp' => null), false)) {
return false;
}
$close_id = $close->id;
}
$this->cacheQueries = true;
$account = $this->find('all', array
('contain' => $contain,
'fields' => array(),
'conditions' =>
$id ? array(array('Account.id' => $id)) : array()
));
$this->cacheQueries = false;
//pr(compact('id', 'account'));
foreach ($account AS $acct) {
if (!$this->Ledger->closeLedger($acct['CurrentLedger']['id'], $close_id))
return false;
}
return true;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: ledgerEntries
* - Returns an array of ledger entries that belong to the given
* account, either just from the current ledger, or from all ledgers.
*/
function ledgerEntries($id, $all = false, $cond = null, $link = null) {
$ledgers = $this->ledgers($id, $all);
return $this->Ledger->ledgerEntries($ledgers, $cond, $link);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: findUnreconciledLedgerEntries
* - Returns ledger entries that are not yet reconciled
* (such as charges not paid).
*/
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);
$set = $this->Ledger->Entry->reconciledSet($set, $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['LedgerEntry'];
// 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;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: reconcileLedgerEntries
* - 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 reconcileLedgerEntries($id, $cond = null) {
$unreconciled = $this->findUnreconciledLedgerEntries($id, null, $cond);
//pr(compact('unreconciled'));
$entry = array();
foreach (array('debit', 'credit') AS $dc)
$entry[$dc] = array('balance' => 0);
while ($entry['debit'] && $entry['credit']) {
// If/When we've exhausted this/these entries, move the next
foreach (array('debit', 'credit') AS $dc) {
if ($entry[$dc]['balance'] <= 0) {
$entry[$dc] =& current($unreconciled[$dc]['entry']);
next($unreconciled[$dc]['entry']);
$entry[$dc]['applied'] = 0;
continue 2;
}
}
// At this point, both entries are valid with a positive balance
$apply = min($entry['debit']['balance'], $entry['credit']['balance']);
// REVISIT <AP>: 20090716
// NOT YET ENTERING THE RECONCILIATION SO THAT WE CAN TEST
// MUST ADD THE RECONCILIATION ENTRY!!!!
foreach (array('debit', 'credit') AS $dc) {
$entry[$dc]['applied'] += $apply;
$entry[$dc]['reconciled'] += $apply;
$entry[$dc]['balance'] -= $apply;
}
}
foreach (array('debit', 'credit') AS $dc) {
$unreconciled[$dc]['applied'] = 0;
//$unreconciled[$dc]['unapplied'] = 0;
foreach ($unreconciled[$dc]['entry'] AS $i => $entry) {
if (isset($entry[$dc]['applied']))
$unreconciled[$dc]['applied'] += $entry[$dc]['applied'];
else
unset($unreconciled[$dc]['entry'][$i]);
//$unreconciled[$dc]['unapplied'] += $entry[$dc]['balance'];
}
$unreconciled[$dc]['balance'] -= $unreconciled[$dc]['applied'];
}
$unreconciled['debit'] ['unapplied'] = $unreconciled['credit']['balance'];
$unreconciled['credit']['unapplied'] = $unreconciled['debit'] ['balance'];
//pr(compact('unreconciled'));
return $unreconciled;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: postLedgerEntry
* -
* transaction_data
* - transaction_id (optional... if set all else is ignored)
* - Transaction
* - stamp (optional... otherwise NOW is used)
* - comment
*
* monetary_source_data
* - monetary_source_id (optional... if set all else is ignored)
* - account_name
* - MonetarySource
* - name
*/
function postLedgerEntry($transaction_data,
$monetary_data,
$entry_data,
$reconcile = null) {
//pr(compact('transaction_data', 'monetary_data', 'entry_data', 'reconcile'));
// Automatically figure out the customer if we have the lease
if (isset($entry_data['lease_id']) && !isset($entry_data['customer_id'])) {
$L = new Lease();
$L->recursive = -1;
$lease = $L->read(null, $entry_data['lease_id']);
$entry_data['customer_id'] = $lease['Lease']['customer_id'];
}
if (!isset($entry_data['lease_id']))
$entry_data['lease_id'] = null;
if (!isset($entry_data['customer_id']))
$entry_data['customer_id'] = null;
// Get the Transaction squared away
if (isset($transaction_data['transaction_id'])) {
$transaction_data
= array_intersect_key($transaction_data,
array('transaction_id'=>1,
'split_transaction_id'=>1));
}
elseif (isset($transaction_data['Transaction'])) {
$transaction_data
= array_intersect_key($transaction_data,
array('Transaction'=>1,
'split_transaction_id'=>1));
}
else {
$transaction_data = array('Transaction'=>array('stamp' => null));
}
// Get the Monetary Source squared away
if (isset($monetary_data)) {
if (!isset($monetary_data['monetary_source_id'])) {
// Convert Account ID to name or vice versa
if (isset($monetary_data['account_id'])) {
$monetary_data['account_name'] = $this->name($monetary_data['account_id']);
} elseif (isset($monetary_data['account_name'])) {
$monetary_data['account_id'] = $this->nameToID($monetary_data['account_name']);
}
if ($monetary_data['account_id'] == $this->cashAccountID()) {
// No distinguishing features of Cash, just
// use the shared monetary source
$monetary_data['monetary_source_id'] =
$this->Ledger->LedgerEntry->MonetarySource->nameToID('Cash');
}
}
if (isset($monetary_data['monetary_source_id'])) {
$monetary_data
= array_intersect_key($monetary_data,
array('monetary_source_id'=>1));
}
else {
// The monetary source needs to be unique
// Create a new one dedicated to this entry
// Give it a fancy name based on the check number
$monetary_data['MonetarySource']['name'] = $monetary_data['account_name'];
if ($monetary_data['account_name'] === $this->name($this->checkAccountID()) ||
$monetary_data['account_name'] === $this->name($this->moneyOrderAccountID())) {
$monetary_data['MonetarySource']['name'] .=
' #' . $monetary_data['MonetarySource']['data1'];
}
$monetary_data
= array_intersect_key($monetary_data,
array('MonetarySource'=>1));
}
}
else {
$monetary_data = array();
}
// Make sure to clean out any unwanted data from the entry
$entry_data
= array_diff_key($entry_data,
array('transaction_id'=>1, 'Transaction'=>1,
'monetary_source_id'=>1, 'MonetarySource'=>1));
// Then add in the transaction and monetary source data
//pr(compact('transaction_data', 'monetary_data', 'entry_data'));
if (isset($transaction_data))
$entry_data += $transaction_data;
if (isset($monetary_data))
$entry_data += $monetary_data;
// Set up the debit ledger id
if (!isset($entry_data['debit_ledger_id'])) {
$entry_data['debit_ledger_id'] =
(isset($entry_data['debit_account_id'])
? $this->currentLedgerID($entry_data['debit_account_id'])
: (isset($entry_data['debit_account_name'])
? $this->currentLedgerID($this->nameToID($entry_data['debit_account_name']))
: null
)
);
}
// Set up the credit ledger id
if (!isset($entry_data['credit_ledger_id'])) {
$entry_data['credit_ledger_id'] =
(isset($entry_data['credit_account_id'])
? $this->currentLedgerID($entry_data['credit_account_id'])
: (isset($entry_data['credit_account_name'])
? $this->currentLedgerID($this->nameToID($entry_data['credit_account_name']))
: null
)
);
}
//pr(array('pre-save', compact('entry_data')));
// Create it!
$new_entry = new LedgerEntry();
$new_entry->create();
if (!$new_entry->saveAll($entry_data, array('validate'=>false))) {
return array('error' => true);
}
// See if the user has entered some sort of non-array
// for the reconcile parameter.
if (isset($reconcile) && is_bool($reconcile) && $reconcile) {
$reconcile = array('debit' => true, 'credit' => true);
}
elseif (isset($reconcile) && $reconcile == 'invoice') {
$reconcile = array('credit' => 'invoice');
}
elseif (isset($reconcile) && $reconcile == 'receipt') {
$reconcile = array('debit' => 'receipt');
}
elseif (!isset($reconcile) || !is_array($reconcile)) {
$reconcile = array();
}
// Reconcile the new entry... assume we'll have success
$err = false;
foreach (array_intersect_key($reconcile, array('credit'=>1,'debit'=>1))
AS $dc_type => $reconcile_set) {
if (!isset($reconcile_set) || (is_bool($reconcile_set) && !$reconcile_set))
continue;
if ($reconcile_set === 'receipt') {
$C = new Customer();
$reconciled = $C->amountWouldReconcile($entry_data['customer_id'],
$this->fundamentalOpposite($dc_type),
$entry_data['amount']);
/* pr(array("reconcile receipt", */
/* compact('reconciled', 'split_transaction', 'transaction_data'))); */
$split_transaction = array_intersect_key($transaction_data,
array('Transaction'=>1,
'split_transaction_id'=>1));
if (isset($split_transaction['split_transaction_id']))
$split_transaction['transaction_id'] = $split_transaction['split_transaction_id'];
if (is_array($reconciled) && count($reconciled[$dc_type]['entry'])) {
foreach ($reconciled[$dc_type]['entry'] AS $rec) {
//pr(compact('rec', 'split_transaction'));
if (!$rec['applied'])
continue;
// Create an entry to handle the splitting of the funds ("Payment")
// and reconcile against the new cash/check/etc entry created above,
// as well as the A/R account.
// Payment must debit the Receipt ledger, and credit the A/R ledger
// debit: Receipt credit: A/R
$ids = $this->postLedgerEntry
($split_transaction,
null,
array('debit_ledger_id' => $this->currentLedgerID($this->receiptAccountID()),
'credit_ledger_id' => $this->currentLedgerID($this->accountReceivableAccountID()),
'amount' => $rec['applied'],
'lease_id' => $rec['lease_id'],
'customer_id' => $rec['customer_id'],
),
array('debit' => array(array('LedgerEntry' => array('id' => $new_entry->id,
'amount' => $rec['applied']))),
'credit' => array(array('LedgerEntry' => array('id' => $rec['id'],
'amount' => $rec['applied']))))
);
// Keep using the same split transaction for all reconciled entries
$split_transaction = array_intersect_key($ids, array('transaction_id'=>1));
//pr(compact('ids', 'split_transaction'));
}
//pr("end reconciled is array");
}
//pr("end reconcile receipt");
}
if (is_array($reconcile_set)) {
//pr("reconcile_set is array");
foreach ($reconcile_set AS $reconcile_entry) {
if (!isset($reconcile_entry['LedgerEntry']['id']))
continue;
$amount = $reconcile_entry['LedgerEntry']['amount'];
if (!$amount)
continue;
if ($dc_type == 'debit') {
$debit_ledger_entry_id = $new_entry->id;
$credit_ledger_entry_id = $reconcile_entry['LedgerEntry']['id'];
}
else {
$debit_ledger_entry_id = $reconcile_entry['LedgerEntry']['id'];
$credit_ledger_entry_id = $new_entry->id;
}
$R = new Reconciliation();
$R->create();
if (!$R->save(compact('amount',
'debit_ledger_entry_id',
'credit_ledger_entry_id'), false))
$err = true;
}
}
}
$new_entry->recursive = -1;
$new_entry->read();
//pr(array('post-save', $entry->data));
$ret = array
('error' => $err,
'id' => $new_entry->data['LedgerEntry']['id'],
'transaction_id' => $new_entry->data['LedgerEntry']['transaction_id'],
'monetary_source_id' => $new_entry->data['LedgerEntry']['monetary_source_id']);
if (isset($split_transaction['transaction_id']))
$ret['split_transaction_id'] = $split_transaction['transaction_id'];
return $ret;
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: closeAndDeposit
* - Closes the current set of ledgers, transferring
* their balances to specified ledger.
*/
function closeAndDeposit($set, $deposit_account_id) {
$close = new Close();
$close->create();
if (!$close->save(array('stamp' => null, 'comment' => 'Deposit'), false)) {
return false;
}
$transaction = array();
foreach ($set AS $ledger) {
// REVISIT <AP>: 20090710
// If the user said to include a ledger in the
// set, should we really be excluding it?
if ($ledger['total'] == 0)
continue;
$ids = $this->postLedgerEntry
($transaction,
null,
array('debit_account_id' => $deposit_account_id,
'credit_ledger_id' => $ledger['id'],
'amount' => $ledger['total']),
// Reconcile the account for cash/check/etc,
// which is the credit side of this entry.
array('credit' => $ledger['entries']));
//pr(compact('ids'));
if ($ids['error'])
die("closeAndDeposit : postLedgerEntry returned error!");
$transaction = array_intersect_key($ids, array('transaction_id'=>1));
$this->Ledger->closeLedger($ledger['id'], $close->id);
}
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: stats
* - Returns summary data from the requested account.
*/
function stats($id = null, $all = false, $query = null) {
if (!$id)
return null;
$this->queryInit($query);
/* if ($all) { */
/* if (!isset($query['link']['Ledger'])) */
/* $query['link']['Ledger'] = array(); */
/* if (!isset($query['link']['Ledger']['fields'])) */
/* $query['link']['Ledger']['fields'] = array(); */
/* $query['link']['Ledger']['fields'][] = 'id'; */
/* } */
/* else { */
/* if (!isset($query['link']['CurrentLedger'])) */
/* $query['link']['CurrentLedger'] = array(); */
/* if (!isset($query['link']['CurrentLedger']['fields'])) */
/* $query['link']['CurrentLedger']['fields'] = array(); */
/* $query['link']['CurrentLedger']['fields'][] = 'id'; */
/* } */
/* $query['conditions'][] = array('Account.id' => $id); */
/* $account = $this->find('first', $query); */
$query['link'] = array('Account' => $query['link']);
foreach ($this->ledgers($id, $all) AS $ledger)
$this->statsMerge($stats['Ledger'],
$this->Ledger->stats($ledger, $query));
/* $stats = array(); */
/* if ($all) { */
/* foreach ($account['Ledger'] AS $ledger) */
/* $this->statsMerge($stats['Ledger'], */
/* $this->Ledger->stats($ledger['id'], $query)); */
/* } */
/* else { */
/* $stats['Ledger'] = */
/* $this->Ledger->stats($account['CurrentLedger']['id'], $query); */
/* } */
return $stats;
}
}
?>