array( 'dependent' => true, ), 'StatementEntry' => array( 'dependent' => true, ), 'DepositTender' => array( 'className' => 'Tender', 'foreignKey' => 'deposit_transaction_id', ), 'Charge' => array( 'className' => 'StatementEntry', 'conditions' => array('Charge.type' => 'CHARGE') ), 'Disbursement' => array( 'className' => 'StatementEntry', 'conditions' => array('Disbursement.type' => 'DISBURSEMENT') ), 'Debit' => array( 'className' => 'LedgerEntry', 'conditions' => array('Debit.crdr' => 'DEBIT') ), 'Credit' => array( 'className' => 'LedgerEntry', 'conditions' => array('Credit.crdr' => 'CREDIT') ), ); var $default_log_level = array('log' => 30, 'show' => 15); /************************************************************************** ************************************************************************** ************************************************************************** * function: addInvoice * - Adds a new invoice invoice */ function addInvoice($data, $customer_id, $lease_id = null) { $this->prEnter(compact('data', 'customer_id', 'lease_id')); // Set up control parameters $data += array('control' => array()); $data['control'] += array('assign' => true, 'include_ledger_entry' => true, 'include_statement_entry' => true, ); // Establish the transaction as an invoice $data['Transaction'] += array('type' => 'INVOICE', 'crdr' => 'DEBIT', 'account_id' => $this->Account->accountReceivableAccountID(), 'customer_id' => $customer_id, 'lease_id' => $lease_id, ); // Go through the statement entries and flag as charges foreach ($data['Entry'] AS &$entry) $entry += array('type' => 'CHARGE', ); $ids = $this->addTransaction($data['control'], $data['Transaction'], $data['Entry']); if (isset($ids['transaction_id'])) $ids['invoice_id'] = $ids['transaction_id']; return $this->prReturn($ids); } /************************************************************************** ************************************************************************** ************************************************************************** * function: addReceipt * - Adds a new receipt */ function addReceipt($data, $customer_id, $lease_id = null) { $this->prEnter(compact('data', 'customer_id', 'lease_id')); // Set up control parameters $data += array('control' => array()); $data['control'] += array('assign' => true, 'assign_receipt' => true, 'include_ledger_entry' => true, 'include_statement_entry' => false, ); // Establish the transaction as a receipt $data['Transaction'] += array('type' => 'RECEIPT', 'crdr' => 'CREDIT', 'account_id' => $this->Account->accountReceivableAccountID(), 'customer_id' => $customer_id, 'lease_id' => $lease_id, ); // Go through the statement entries and flag as disbursements foreach ($data['Entry'] AS &$entry) $entry += array('type' => 'DISBURSEMENT', // not used 'account_id' => (isset($entry['Tender']['tender_type_id']) ? ($this->LedgerEntry->Tender->TenderType-> accountID($entry['Tender']['tender_type_id'])) : null), ); $ids = $this->addTransaction($data['control'], $data['Transaction'], $data['Entry']); if (isset($ids['transaction_id'])) $ids['receipt_id'] = $ids['transaction_id']; return $this->prReturn($ids); } /************************************************************************** ************************************************************************** ************************************************************************** * function: addWaiver * - Adds a new waiver */ function addWaiver($data, $charge_id, $customer_id, $lease_id = null) { $this->prEnter(compact('data', 'charge_id', 'customer_id', 'lease_id')); if (count($data['Entry']) != 1) $this->INTERNAL_ERROR("Should be one Entry for addWaiver"); // No assignment of credits, as we'll manually assign // using charge_entry_id as part of the entry (below). $data += array('control' => array()); $data['control'] += array('assign' => false, 'include_ledger_entry' => true, 'include_statement_entry' => true, ); // Just make sure the disbursement(s) are marked as waivers // and that they go to cover the specific charge. $data['Entry'][0] += array('type' => 'WAIVER', 'account_id' => $this->Account->waiverAccountID(), 'charge_entry_id' => $charge_id); // In all other respects this is just a receipt. $ids = $this->addReceipt($data, $customer_id, $lease_id); if (isset($ids['transaction_id'])) $ids['waiver_id'] = $ids['transaction_id']; return $this->prReturn($ids); } /************************************************************************** ************************************************************************** ************************************************************************** * function: addWriteOff * - Adds a new write off of bad debt */ function addWriteOff($data, $customer_id, $lease_id = null) { $this->prEnter(compact('data', 'customer_id', 'lease_id')); if (count($data['Entry']) != 1) $this->INTERNAL_ERROR("Should be one Entry for addWriteOff"); // Just make sure the disbursement(s) are marked as write offs // and that the write-off account is used for the charge. $data['Entry'][0] += array('type' => 'WRITEOFF', 'account_id' => $this->Account->badDebtAccountID()); // In all other respects this is just a receipt. $ids = $this->addReceipt($data, $customer_id, $lease_id); if (isset($ids['transaction_id'])) $ids['writeoff_id'] = $ids['transaction_id']; return $this->prReturn($ids); } /************************************************************************** ************************************************************************** ************************************************************************** * function: addDeposit * - Adds a new bank deposit */ function addDeposit($data, $account_id) { $this->prEnter(compact('data', 'account_id')); // Set up control parameters $data += array('control' => array()); $data['control'] += array('assign' => false, 'include_ledger_entry' => true, 'include_statement_entry' => false, ); // Establish the transaction as a deposit $data['Transaction'] += array('type' => 'DEPOSIT', 'crdr' => 'DEBIT', 'account_id' => $account_id, 'customer_id' => null, 'lease_id' => null, ); // Save the list of IDs, so that we can mark their // deposit transaction after it has been created. $tender_ids = array_map(create_function('$item', 'return $item["tender_id"];'), $data['Entry']); // Go through the statement entries and re-group by account id $group = array(); foreach ($data['Entry'] AS &$entry) { if (!isset($group[$entry['account_id']])) $group[$entry['account_id']] = array('account_id' => $entry['account_id'], 'amount' => 0); $group[$entry['account_id']]['amount'] += $entry['amount']; } $data['Entry'] = $group; $ids = $this->addTransaction($data['control'], $data['Transaction'], $data['Entry']); if (isset($ids['transaction_id'])) $ids['deposit_id'] = $ids['transaction_id']; if (!empty($ids['deposit_id'])) { $this->LedgerEntry->Tender->updateAll (array('Tender.deposit_transaction_id' => $ids['deposit_id']), array('Tender.id' => $tender_ids) ); } return $this->prReturn($ids); } /************************************************************************** ************************************************************************** ************************************************************************** * function: addClose * - Adds a new transaction for closing ledgers */ function addClose($data) { $this->prEnter(compact('data')); // Set up control parameters $data += array('control' => array()); $data['control'] += array('assign' => false, 'include_ledger_entry' => true, 'include_statement_entry' => false, ); // Establish the transaction as a close $data['Transaction'] += array('type' => 'CLOSE', 'crdr' => null, 'account_id' => null, 'customer_id' => null, 'lease_id' => null, ); $ledger_ids = array(); $data['Entry'] = array(); foreach ($data['Ledger'] AS $ledger) { $ledger_id = $ledger['old_ledger_id']; $new_ledger_id = $ledger['new_ledger_id']; $amount = $ledger['amount']; $account_id = $this->Account->Ledger->accountID($ledger_id); $crdr = strtoupper($this->Account->fundamentalOpposite($account_id)); $comment = "Ledger Carry Forward (c/f)"; // Save the ledger ID for later, to mark it as closed $ledger_ids[] = $ledger_id; // No need to generate ledger entries if there is no balance if (empty($ledger['amount']) || $ledger['amount'] == 0) continue; // Add an entry to carry the ledger balance forward $data['Entry'][] = compact('account_id', 'ledger_id', 'new_ledger_id', 'crdr', 'amount', 'comment'); } unset($data['Ledger']); // Add the transaction and carry forward balances $ids = $this->addTransaction($data['control'], $data['Transaction'], $data['Entry']); if (isset($ids['transaction_id'])) $ids['close_id'] = $ids['transaction_id']; // Mark the older ledgers as closed if (!empty($ids['close_id'])) { $this->LedgerEntry->Ledger->updateAll (array('Ledger.close_transaction_id' => $ids['close_id']), array('Ledger.id' => $ledger_ids) ); } return $this->prReturn($ids); } /************************************************************************** ************************************************************************** ************************************************************************** * function: addRefund * - Adds a new refund */ function addRefund($data, $customer_id, $lease_id = null) { $this->prEnter(compact('data', 'customer_id', 'lease_id')); // Set up control parameters $data += array('control' => array()); $data['control'] += array('assign' => true, ); // Establish the transaction as a Refund. This is just like a // Payment, except instead of paying out of the account payable, // it comes from the customer credit in the account receivable. // Someday, perhaps we'll just issue a Credit Note or similar, // but for now, a refund means it's time to actually PAY. $data['Transaction'] += array('account_id' => $this->Account->accountReceivableAccountID()); // Also, to make it clear to the user, we flag as a REFUND // even though that type works and operates just as PAYMENT foreach ($data['Entry'] AS &$entry) $entry += array('type' => 'REFUND'); $ids = $this->addPayment($data, $customer_id, $lease_id); if (isset($ids['transaction_id'])) $ids['refund_id'] = $ids['transaction_id']; return $this->prReturn($ids); } /************************************************************************** ************************************************************************** ************************************************************************** * function: addPayment * - Adds a new payment transaction, which is money outflow */ function addPayment($data, $customer_id, $lease_id = null) { $this->prEnter(compact('data', 'customer_id', 'lease_id')); // Set up control parameters $data += array('control' => array()); $data['control'] += array('assign' => false, 'include_ledger_entry' => true, 'include_statement_entry' => true, ); // Establish the transaction as an payment $data['Transaction'] += array('type' => 'PAYMENT', 'crdr' => 'DEBIT', 'account_id' => $this->Account->accountPayableAccountID(), 'customer_id' => $customer_id, 'lease_id' => $lease_id, ); // Go through the statement entries and flag as payments foreach ($data['Entry'] AS &$entry) $entry += array('type' => 'PAYMENT', ); $ids = $this->addTransaction($data['control'], $data['Transaction'], $data['Entry']); if (isset($ids['transaction_id'])) $ids['payment_id'] = $ids['transaction_id']; return $this->prReturn($ids); } /************************************************************************** ************************************************************************** ************************************************************************** * function: verifyTransaction * - Verifies consistenty of new transaction data * (not in a pre-existing transaction) */ function verifyTransaction($transaction, $entries) { //$this->prFunctionLevel(10); $this->prEnter(compact('transaction', 'entries')); // Verify required Transaction data is present if (empty($transaction['type']) || ($transaction['type'] != 'CLOSE' && (empty($transaction['account_id']) || empty($transaction['crdr']))) || (in_array($transaction['type'], array('INVOICE', 'RECEIPT')) && empty($transaction['customer_id'])) ) { return $this->prReturn(false); } // Verify all entries foreach ($entries AS $entry) { // Ensure these items are null'ed out so we don't // accidentally pick up stale data. $le1 = $le1_tender = $le2 = $se = null; extract($entry); if (!empty($le1) && !empty($le2) && !$this->LedgerEntry->DoubleEntry->verifyDoubleEntry($le1, $le2, $le1_tender)) { return $this->prReturn(false); } if (!empty($se) && !$this->StatementEntry->verifyStatementEntry($se)) { return $this->prReturn(false); } } return $this->prReturn(true); } /************************************************************************** ************************************************************************** ************************************************************************** * function: addTransaction * - Adds a new transaction, and the appropriate ledger and statement * entries, as layed out in the $data['Entry'] array. The array is * overloaded, since it is used to create both ledger _and_ statement * entries. */ function addTransaction($control, $transaction, $entries) { $this->prEnter(compact('control', 'transaction', 'entries')); // Verify that we have a transaction and entries if (empty($transaction) || ($transaction['type'] !== 'CLOSE' && empty($entries))) return $this->prReturn(array('error' => true)); // set ledger ID as the current ledger of the specified account if (empty($transaction['ledger_id'])) $transaction['ledger_id'] = $this->Account->currentLedgerID($transaction['account_id']); // Automatically figure out the customer if we have the lease if (!empty($transaction['lease_id']) && empty($transaction['customer_id'])) { $L = new Lease(); $L->id = $transaction['lease_id']; $transaction['customer_id'] = $L->field('customer_id'); } $result = $this->_splitEntries($control, $transaction, $entries); if (!empty($result['error'])) return $this->prReturn(array('error' => true)); // Extract each item from result and make local variables extract($result); // If transaction amount is not already set, use the // sum of the entry amounts. $transaction += array('amount' => $transaction_amount); $this->pr(20, compact('transaction', 'entries')); // Move forward, verifying and saving everything. $ret = array(); if (!$this->verifyTransaction($transaction, $entries)) return $this->prReturn(array('error' => true) + $ret); // Save transaction to the database $this->create(); if (!$this->save($transaction)) return $this->prReturn(array('error' => true) + $ret); $transaction['id'] = $this->id; // Add the entries $ret = $this->addTransactionEntries($control, $transaction, $entries, false); if (!empty($control['assign']) && !$ret['error']) { $result = $this->StatementEntry->assignCredits (null, (empty($control['assign_receipt']) ? null : $ret['transaction_id']), null, $assign_disbursement_type, $transaction['customer_id'], $transaction['lease_id'] ); $ret['assigned'] = $result; if ($result['error']) $ret['error'] = true; if (!empty($control['assign_receipt']) && !empty($result['receipt_balance'])) { $result = $this->addTransactionEntries (array('include_ledger_entry' => true, 'include_statement_entry' => true), array('crdr' => 'DEBIT') + $transaction, array(array('type' => 'SURPLUS', 'account_id' => $this->Account->accountPayableAccountID(), 'amount' => $result['receipt_balance'], ), )); } } $this->Customer->update($transaction['customer_id']); return $this->prReturn($ret); } /************************************************************************** ************************************************************************** ************************************************************************** * function: addTransactionEntries * - Largely a helper function to addTransaction, this function is * responsible for adding ledger/statement entries to an existing * transaction. If needed, this function can also be called outside * of addTransaction, although it's not clear where that would be * appropriate, since transactions are really snapshots of some * event, and shouldn't be mucked with after creation. */ function addTransactionEntries($control, $transaction, $entries, $split = true) { $this->prEnter(compact('control', 'transaction', 'entries', 'split')); // Verify that we have a transaction and entries if (empty($transaction['id']) || ($transaction['type'] !== 'CLOSE' && empty($entries))) return $this->prReturn(array('error' => true)); // If the entries are not already split, do so now. if ($split) { $result = $this->_splitEntries($control, $transaction, $entries); if (!empty($result['error'])) return $this->prReturn(array('error' => true)); $entries = $result['entries']; } /* // Verify the entries */ /* $ret = array(); */ /* if (!$this->verifyTransaction($transaction, $entries)) */ /* return $this->prReturn(array('error' => true) + $ret); */ $this->id = $transaction['id']; $transaction['stamp'] = $this->field('stamp'); $transaction['customer_id'] = $this->field('customer_id'); // Set up our return array $ret = array(); $ret['transaction_id'] = $this->id; $ret['entries'] = array(); $ret['error'] = false; // Go through the entries foreach ($entries AS $e_index => &$entry) { // Ensure these items are null'ed out so we don't // accidentally pick up stale data. $le1 = $le1_tender = $le2 = $se = null; extract($entry); if (!empty($le1) && !empty($le2)) { $le1['transaction_id'] = $le2['transaction_id'] = $ret['transaction_id']; if (isset($le1_tender)) $le1_tender['customer_id'] = $transaction['customer_id']; $result = $this->LedgerEntry->DoubleEntry->addDoubleEntry($le1, $le2, $le1_tender); $ret['entries'][$e_index]['DoubleEntry'] = $result; if ($result['error']) { $ret['error'] = true; continue; } } if (!empty($se)) { $se['transaction_id'] = $ret['transaction_id']; if (empty($se['effective_date'])) $se['effective_date'] = $transaction['stamp']; $result = $this->StatementEntry->addStatementEntry($se); $ret['entries'][$e_index]['StatementEntry'] = $result; if ($result['error']) { $ret['error'] = true; continue; } } } return $this->prReturn($ret); } /************************************************************************** ************************************************************************** ************************************************************************** * function: _splitEntries * - An internal helper function capable of splitting an array of * combined ledger/statement entries into their indivdual entry * components (2 ledger entries, and a statement entry), based * on the typical requirements. Any custom split will have to * be done outside of this function. */ function _splitEntries($control, $transaction, $entries) { $this->prEnter(compact('control', 'transaction', 'entries')); // Verify that we have a transaction and entries if (empty($transaction) || ($transaction['type'] !== 'CLOSE' && empty($entries))) return $this->prReturn(array('error' => true)); // Some transactions do not have their statement entries // generated directly as part of the transaction, but are // created in the final steps during the reconciliation // phase by the assignCredits function. Keep track of // what type the statement entries _would_ have been, so // that the assignCredits function can do the same. $assign_disbursement_type = null; // Break each entry out of the combined statement/ledger entry // and into individual entries appropriate for saving. While // we're at it, calculate the transaction total as well. $transaction_amount = 0; foreach ($entries AS &$entry) { // Ensure these items are null'ed out so we don't // accidentally pick up stale data. $le1 = $le1_tender = $le2 = $se = null; // Really, data should be sanitized at the controller, // and not here. However, it's a one stop cleanup. $entry['amount'] = str_replace('$', '', $entry['amount']); // Set up our comments, possibly using the default 'comment' field if (empty($entry['ledger_entry_comment'])) { if ($transaction['type'] != 'INVOICE' && !empty($entry['comment'])) $entry['ledger_entry_comment'] = $entry['comment']; else $entry['ledger_entry_comment'] = null; } if (empty($entry['statement_entry_comment'])) { if ($transaction['type'] == 'INVOICE' && !empty($entry['comment'])) $entry['statement_entry_comment'] = $entry['comment']; else $entry['statement_entry_comment'] = null; } if (empty($transaction['crdr']) && !empty($transaction['account_id'])) $transaction['crdr'] = strtoupper($this->Account->fundamentalType ($transaction['account_id'])); if (empty($entry['crdr']) && !empty($transaction['crdr'])) $entry['crdr'] = strtoupper($this->Account->fundamentalOpposite ($transaction['crdr'])); // Priority goes to settings defined in $entry, but // use the control information as defaults. $entry += $control; if (!empty($entry['include_ledger_entry'])) { // Create one half of the Double Ledger Entry (and the Tender) $le1 = array_intersect_key($entry, array_flip(array('ledger_id', 'account_id', 'crdr', 'amount'))); $le1['comment'] = $entry['ledger_entry_comment']; $le1_tender = isset($entry['Tender']) ? $entry['Tender'] : null; // Create the second half of the Double Ledger Entry if ($transaction['type'] == 'CLOSE') { $le2 = array_intersect_key($entry, array_flip(array('account_id', 'amount'))); $le2['ledger_id'] = $entry['new_ledger_id']; $le2['crdr'] = strtoupper($this->Account->fundamentalType($le2['account_id'])); $le2['comment'] = "Ledger Balance Forward (b/f)"; } else { $le2 = array_intersect_key($entry, array_flip(array('amount'))) + array_intersect_key($transaction, array_flip(array('ledger_id', 'account_id', 'crdr'))); } } else $le1 = $le1_tender = $le2 = null; // Now that the ledger entries are in place, respect the 'negative' flag if (!empty($entry['negative'])) $entry['amount'] *= -1; if (!empty($entry['include_statement_entry'])) { // Create the statement entry $se = array_intersect_key($entry, array_flip(array('type', 'account_id', 'amount', 'effective_date', 'through_date', 'due_date', 'customer_id', 'lease_id', 'charge_entry_id'))) + array_intersect_key($transaction, array_flip(array('customer_id', 'lease_id'))); $se['comment'] = $entry['statement_entry_comment']; } else { if (!empty($entry['assign']) && !empty($entry['type']) && empty($entry['charge_entry_id'])) { if (empty($assign_disbursement_type)) $assign_disbursement_type = $entry['type']; elseif ($entry['type'] != $assign_disbursement_type) $this->INTERNAL_ERROR('Multiple disbursement types for this transaction'); } $se = null; } // Add entry amount into the transaction total $transaction_amount += $entry['amount']; // Replace combined entry with our new individual entries $entry = compact('le1', 'le1_tender', 'le2', 'se'); } return $this->prReturn(compact('transaction_amount', 'assign_disbursement_type', 'entries') + array('error' => false)); } /************************************************************************** ************************************************************************** ************************************************************************** * function: addNsf * - Adds NSF transaction */ function addNsf($tender, $stamp = null, $comment = null) { $this->prEnter(compact('tender', 'stamp', 'comment')); $ret = array(); // Enter the NSF // This is the transaction pulling money from the bank account // and recording it in the NSF account. It has nothing to do // with the customer statement (charges, disbursements, credits, etc). $bounce_result = $this->addDeposit (array('Transaction' => array('stamp' => $stamp, 'type' => 'WITHDRAWAL', 'crdr' => 'CREDIT'), 'Entry' => array(array('tender_id' => null, 'account_id' => $this->Account->nsfAccountID(), 'amount' => $tender['LedgerEntry']['amount'], ))), $tender['Transaction']['account_id']); $this->pr(20, compact('bounce_result')); $ret['bounce'] = $bounce_result; if ($bounce_result['error']) return $this->prReturn(array('error' => true) + $ret); // Since we may have saved the nsf transaction with a null // timestamp, query it back out of the database to find out // what timestamp was _really_ specified, for later use. $bounce = $this->find ('first', array('contain' => false, 'id' => $bounce_result['transaction_id'])); $this->pr(20, compact('bounce')); $stamp = $bounce['Transaction']['stamp']; // OK, now move into customer realm, finding all statement // entries that were affected by the bad payment (tender). $nsf_ledger_entry = $this->LedgerEntry->find ('first', array ('contain' => array('Transaction' => array(//'fields' => array(), 'StatementEntry' => array(//'fields' => array(), ), ), ), 'conditions' => array('LedgerEntry.id' => $tender['LedgerEntry']['id']), )); $this->pr(20, compact('nsf_ledger_entry')); if (!$nsf_ledger_entry) return $this->prReturn(array('error' => true) + $ret); // Build a transaction to adjust all of the statement entries $rollback = array('control' => array('assign' => false, 'include_ledger_entry' => false, 'include_statement_entry' => true, ), 'Transaction' => array('stamp' => $stamp, 'type' => 'RECEIPT', 'crdr' => 'CREDIT', 'account_id' => $this->Account->nsfAccountID(), 'customer_id' => $tender['Tender']['customer_id'], 'comment' => $comment, ), 'Entry' => array()); $rollback['Transaction']['amount'] = 0; foreach ($nsf_ledger_entry['Transaction']['StatementEntry'] AS $disbursement) { if ($disbursement['type'] === 'SURPLUS') { $disbursement['type'] = 'VOID'; $this->StatementEntry->id = $disbursement['id']; $this->StatementEntry->saveField('type', $disbursement['type']); } else { $rollback['Entry'][] = array('type' => $disbursement['type'], 'amount' => -1 * $disbursement['amount'], 'account_id' => $this->Account->nsfAccountID(), 'customer_id' => $disbursement['customer_id'], 'lease_id' => $disbursement['lease_id'], 'charge_entry_id' => $disbursement['charge_entry_id'], ); $rollback['Transaction']['amount'] += $disbursement['amount']; } } // Add the sole ledger entry for this transaction $rollback['Entry'][] = array('include_ledger_entry' => true, 'include_statement_entry' => false, 'amount' => $rollback['Transaction']['amount'], 'account_id' => $this->Account->accountReceivableAccountID(), ); // Set the transaction amount to be negative $rollback['Transaction']['amount'] *= -1; // Record the transaction, which will un-pay previously paid // charges, void any credits, and other similar work. if (count($rollback['Entry'])) { $rollback_result = $this->addTransaction($rollback['control'], $rollback['Transaction'], $rollback['Entry']); $this->pr(20, compact('rollback', 'rollback_result')); $ret['rollback'] = $rollback_result; if ($rollback_result['error']) return $this->prReturn(array('error' => true) + $ret); } // Add NSF Charge $charge_result = $this->addInvoice (array('Transaction' => compact('stamp'), 'Entry' => array (array('account_id' => $this->Account->nsfChargeAccountID(), 'effective_date' => $stamp, // REVISIT : 20090730 // BAD, BAD, BAD... who would actually // hardcode a value like this???? ;-) 'amount' => 35, 'comment' => "NSF: " . $tender['Tender']['name'], ), ), ), $tender['Tender']['customer_id']); $this->pr(20, compact('charge_result')); $ret['charge'] = $charge_result; if ($charge_result['error']) return $this->prReturn(array('error' => true) + $ret); $ret['nsf_transaction_id'] = $ret['bounce']['transaction_id']; if (!empty($ret['rollback'])) $ret['nsf_ledger_entry_id'] = $ret['rollback']['entries'][0]['DoubleEntry']['Entry1']['ledger_entry_id']; return $this->prReturn($ret + array('error' => false)); } /************************************************************************** ************************************************************************** ************************************************************************** * function: addReversal * - Adds a new charge reversal */ function addReversal($charge, $stamp = null, $comment = null) { $this->prEnter(compact('charge', 'stamp', 'comment')); $ret = array(); // Finding all statement entries affected by reversing this charge. $disb_entries = $this->StatementEntry->find ('first', array ('contain' => array('DisbursementEntry' => array(//'fields' => array(), ), ), 'conditions' => array('StatementEntry.id' => $charge['StatementEntry']['id']), )); $this->pr(20, compact('disb_entries')); if (!$disb_entries) return $this->prReturn(array('error' => true) + $ret); // Build a transaction to adjust all of the statement entries // These are all disbursements against the charge we're reversing $rollback = array('control' => array('assign' => true, 'assign_receipt' => true, //'assign_receipt' => true, 'include_ledger_entry' => false, 'include_statement_entry' => true, ), 'Transaction' => array('stamp' => $stamp, 'type' => 'CREDIT_NOTE', 'crdr' => 'CREDIT', 'account_id' => $this->Account->accountReceivableAccountID(), 'amount' => $charge['StatementEntry']['amount'], 'customer_id' => $charge['StatementEntry']['customer_id'], 'lease_id' => null, 'comment' => $comment, ), 'Entry' => array()); foreach ($disb_entries['DisbursementEntry'] AS $disbursement) { $rollback['Entry'][] = array( 'type' => $disbursement['type'], //'type' => 'REVERSAL', 'amount' => -1 * $disbursement['amount'], 'account_id' => $disbursement['account_id'], 'customer_id' => $disbursement['customer_id'], 'lease_id' => $disbursement['lease_id'], 'charge_entry_id' => $disbursement['charge_entry_id'], ); } // Add the sole ledger entry for this transaction $rollback['Entry'][] = array('include_ledger_entry' => true, 'include_statement_entry' => true, 'type' => 'REVERSAL', 'account_id' => $charge['StatementEntry']['account_id'], 'amount' => $rollback['Transaction']['amount'], 'customer_id' => $charge['StatementEntry']['customer_id'], 'lease_id' => $charge['StatementEntry']['lease_id'], 'charge_entry_id' => $charge['StatementEntry']['id'], ); // Record the transaction, which will un-disburse previously // disbursed payments, and other similar work. if (count($rollback['Entry'])) { $rollback_result = $this->addTransaction($rollback['control'], $rollback['Transaction'], $rollback['Entry']); $this->pr(20, compact('rollback', 'rollback_result')); $ret = $rollback_result; if ($rollback_result['error']) return $this->prReturn(array('error' => true) + $ret); } return $this->prReturn($ret + array('error' => false)); } /************************************************************************** ************************************************************************** ************************************************************************** * function: stats * - Deletes a transaction and associated entries * - !!WARNING!! This should be used with EXTREME caution, as it * irreversibly destroys the data. It is not for normal use, * and can leave the database in an inconsistent state. Expected * scenario is to remove a bad transaction directly after creation, * before it gets tied to anything else in the system (such as * a late charge invoice for a customer that isn't actually late). */ function destroy($id) { $this->prFunctionLevel(30); $this->prEnter(compact('id')); /* $transaction = $this->find */ /* ('first', */ /* array('contain' => */ /* array(// Models */ /* 'StatementEntry', */ /* 'LedgerEntry' => array('Tender'), */ /* ), */ /* 'conditions' => array(array('Transaction.id' => $id)), */ /* )); */ /* pr($transaction); */ $this->id = $id; $customer_id = $this->field('customer_id'); $result = $this->delete($id); if (!empty($customer_id)) { $this->StatementEntry->assignCredits (null, null, null, null, $customer_id, null); //$this->Customer->update($customer_id); } return $this->prReturn($result); } /************************************************************************** ************************************************************************** ************************************************************************** * function: update * - Update any cached or calculated fields */ function update($id) { $this->INTERNAL_ERROR("Transaction::update not yet implemented"); $result = $this->find ('first', array('link' => array('StatementEntry'), 'fields' => array("SUM(LedgerEntry.amount) AS total"), 'conditions' => array(array('LedgerEntry.account_id = Transaction.account_id'), ), )); } /************************************************************************** ************************************************************************** ************************************************************************** * function: stats * - Returns summary data from the requested transaction */ function stats($id = null, $query = null, $balance_account_id = null) { $this->prEnter(compact('id', 'query')); $this->queryInit($query); unset($query['group']); if (isset($id)) { $query['conditions'][] = array('Transaction.id' => $id); $query['group'] = 'Transaction.id'; } else // CakePHP seems to automagically add in our ID as a part // of the query conditions, but only on a 'first' query, // not an 'all'. I suppose this is helpful :-/ unset($this->id); if (empty($query['fields'])) $query['fields'] = array(); $stats = array(); foreach ($this->hasMany AS $table => $association) { // Only calculate stats for *Entry types if (!preg_match("/Entry$/", $table) && !preg_match("/Entry$/", $association['className'])) continue; $squery = $query; $squery['link'][$table] = array('fields' => array()); if ($table == 'LedgerEntry') { if (isset($balance_account_id)) { $squery['link']['LedgerEntry']['Account'] = array('fields' => array()); $squery['conditions'][] = array("Account.id" => $balance_account_id); } $squery['fields'] = array_merge($squery['fields'], $this->LedgerEntry->debitCreditFields(true, $balance_account_id != null)); } elseif ($table == 'StatementEntry') { $squery['fields'] = array_merge($squery['fields'], $this->StatementEntry->chargeDisbursementFields(true)); } else { $squery['fields'][] = "SUM({$table}.amount) AS total"; $squery['fields'][] = "COUNT({$table}.id) AS entries"; } $stats[$table] = $this->find('first', $squery); // REVISIT : 20090724 // [0][0] is for when we do an 'all' query. This can // be removed at some point, but I'm keeping it while // toggling between 'all' and 'first' (testing). if (isset($stats[$table][0][0])) $stats[$table] += $stats[$table][0][0]; else $stats[$table] += $stats[$table][0]; unset($stats[$table][0]); } return $this->prReturn($stats); } } ?>