array('numeric'), 'name' => array('notempty'), 'external_name' => array('notempty') ); var $hasOne = array( 'CurrentLedger' => array( 'className' => 'Ledger', // REVISIT 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', ); /************************************************************************** ************************************************************************** ************************************************************************** * 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: 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 accountReceivableAccountID() { return $this->nameToID('A/R'); } function invoiceAccountID() { return $this->nameToID('Invoice'); } function receiptAccountID() { return $this->nameToID('Receipt'); } /************************************************************************** ************************************************************************** ************************************************************************** * function: relatedAccounts * - Returns an array of accounts related by similar attributes */ function relatedAccounts($attribute) { $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) )); $this->cacheQueries = false; return $account; } /************************************************************************** ************************************************************************** ************************************************************************** * 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: findLedgerEntries * - Returns an array of ledger entries that belong to the given * account, either just from the current ledger, or from all ledgers. */ function findLedgerEntries($id, $all = false, $cond = null, $link = null) { /* pr(array('function' => 'Account::findLedgerEntries', */ /* 'args' => compact('id', 'all', 'cond', 'link'), */ /* )); */ $entries = array(); foreach ($this->ledgers($id, $all) AS $ledger_id) { $ledger_entries = $this->Ledger->findLedgerEntries ($ledger_id, $this->type($id), $cond, $link); $entries = array_merge($entries, $ledger_entries); } $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->LedgerEntry->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; } /************************************************************************** ************************************************************************** ************************************************************************** * function: findUnreconciledLedgerEntries * - Returns ledger entries that are not yet reconciled * (such as charges not paid). */ function findUnreconciledLedgerEntries($id = null, $fundamental_type = null, $cond = null) { if (!isset($cond)) $cond = 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(), "LedgerEntry" => array ('class' => "{$ucfund}LedgerEntry", 'fields' => array('id', 'customer_id', 'lease_id', 'amount'), "ReconciliationLedgerEntry" => array ('class' => "{$ucfund}ReconciliationLedgerEntry", 'fields' => array ("COALESCE(SUM(Reconciliation.amount),0) AS 'reconciled'", "LedgerEntry.amount - COALESCE(SUM(Reconciliation.amount),0) AS 'balance'", ), ), ), ), ), 'group' => ("LedgerEntry.id" . " HAVING LedgerEntry.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["LedgerEntry"], array(0=>true)), $entry[0]); $balance += $entry['balance']; } $unreconciled[$fund]['balance'] = $balance; } return $unreconciled; } /************************************************************************** ************************************************************************** ************************************************************************** * function: reconcileNewLedgerEntry * - Returns which ledger entries a new credit/debit would * reconcile, and how much. * * - REVISIT 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 reconcileNewLedgerEntry($id, $fundamental_type, $amount, $cond = null) { $ofund = $this->fundamentalOpposite($fundamental_type); $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); 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; } $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; 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) * - monetary_type_name * - MonetarySource * - name * - monetary_type_id */ function postLedgerEntry($transaction_data, $monetary_data, $entry_data) { /* if (!isset($entry_data) || */ /* !isset($entry_data['amount']) || */ /* !$entry_data['amount']) */ /* return false; */ $A = new Account(); /* // Create a transaction if necessary */ /* if (!isset($transaction_data['id'])) { */ /* $transaction = new Transaction(); */ /* $transaction->create(); */ /* if (!$transaction->save($transaction_data, false)) { */ /* return false; */ /* } */ /* $transaction_data['id'] = $transaction->id; */ /* } */ // Get the Transaction squared away if (isset($transaction_data['transaction_id'])) { $transaction_data = array_intersect_key($transaction_data, array('transaction_id'=>1)); } elseif (isset($transaction_data['Transaction'])) { $transaction_data = array_intersect_key($transaction_data, array('Transaction'=>1)); } else { $transaction_data = array('Transaction'=>array('stamp' => null)); } // Get the Monetary Source squared away if (isset($monetary_data['monetary_source_id'])) { $monetary_data = array_intersect_key($monetary_data, array('monetary_source_id'=>1)); } elseif (isset($monetary_data['monetary_type_name'])) { if ($monetary_data['monetary_type_name'] === 'Cash') { // No distinguishing features of Cash, just // use the shared monetary source $monetary_data['monetary_source_id'] = $this->Ledger->LedgerEntry->MonetarySource->nameToID('Cash'); $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 $monetary_data['MonetarySource']['monetary_type_id'] = $this->Ledger->LedgerEntry->MonetarySource->MonetaryType ->nameToID($monetary_data['monetary_type_name']); $monetary_data['MonetarySource']['name'] = $this->Ledger->LedgerEntry->MonetarySource->MonetaryType ->nameToID($monetary_data['monetary_type_name']); // Give it a fancy name based on the check number $monetary_data['MonetarySource']['name'] = $monetary_data['monetary_type_name']; if ($monetary_data['monetary_type_name'] === 'Check' || $monetary_data['monetary_type_name'] === 'Money Order') { $monetary_data['MonetarySource']['name'] .= ' #' . $monetary_data['MonetarySource']['data1']; } $monetary_data = array_intersect_key($monetary_data, array('MonetarySource'=>1)); } } elseif (isset($monetary_data)) { $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']) ? $A->currentLedgerID($entry_data['debit_account_id']) : (isset($entry_data['debit_account_name']) ? $A->currentLedgerID($A->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']) ? $A->currentLedgerID($entry_data['credit_account_id']) : (isset($entry_data['credit_account_name']) ? $A->currentLedgerID($A->nameToID($entry_data['credit_account_name'])) : null ) ); } //pr(array('pre-save', compact('entry_data'))); // Create it! $entry = new LedgerEntry(); $entry->create(); if (!$entry->saveAll($entry_data, array('validate'=>false))) { return false; } $entry->read(); //pr(array('post-save', $entry->data)); return array('transaction_id' => $entry->data['LedgerEntry']['transaction_id'], 'monetary_source_id' => $entry->data['LedgerEntry']['monetary_source_id'], 'id' => $entry->data['LedgerEntry']['id']); } /************************************************************************** ************************************************************************** ************************************************************************** * 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) { $ids = $this->postLedgerEntry ($transaction, null, array('debit_account_id' => $deposit_account_id, 'credit_ledger_id' => $ledger['ledger_id'], 'amount' => $ledger['amount'])); $transaction = array_intersect_key($ids, array('transaction_id'=>1)); $this->Ledger->closeLedger($ledger['ledger_id'], $close->id); } } /************************************************************************** ************************************************************************** ************************************************************************** * function: stats * - Returns summary data from the requested account. */ function stats($id = null, $all = false, $cond = null) { if (!$id) return null; // All old, closed ledgers MUST balance to 0. // However, the user may want the ENTIRE running totals, // (not just the balance), so we may have to query all // ledgers, as dictated by the $all parameter. $account = $this->find('first', array('contain' => ($all ? array('Ledger' => array ('fields' => array('id'))) : array('CurrentLedger' => array ('fields' => array('id'))) ), 'conditions' => array (array('Account.id' => $id)) )); $stats = array(); if ($all) { foreach ($account['Ledger'] AS $ledger) $this->statsMerge($stats['Ledger'], $this->Ledger->stats($ledger['id'], $cond)); } else { $stats['Ledger'] = $this->Ledger->stats($account['CurrentLedger']['id'], $cond); } return $stats; } } ?>