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', '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, $balance = true, $entry_name = 'LedgerEntry', $account_name = 'Account') { return $this->LedgerEntry->debitCreditFields ($sum, $balance, $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; $accounts = $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; // Rearrange to be of the form (id => name) $rel_accounts = array(); foreach ($accounts AS $acct) { $rel_accounts[$acct['Account']['id']] = $acct['Account']['name']; } return $rel_accounts; } /************************************************************************** ************************************************************************** ************************************************************************** * function: xxxAccounts * - Returns an array of accounts suitable for activity xxx */ function chargeAccounts() { return $this->relatedAccounts('charges', array('order' => 'name')); } function paymentAccounts() { return $this->relatedAccounts('payments', array('order' => 'name')); } function depositAccounts() { return $this->relatedAccounts('deposits', array('order' => 'name')); } /************************************************************************** ************************************************************************** ************************************************************************** * 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 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 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 : 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 : 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; } } ?>