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: 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: 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: 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) * - 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->reconcileNewLedgerEntry($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 : 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, $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; } } ?>