From e784931fa82bcedacb710b152f2e91989562805a Mon Sep 17 00:00:00 2001 From: abijah Date: Thu, 6 Aug 2009 05:11:58 +0000 Subject: [PATCH] Implemented refund, at least for the most part. Minor testing, but looks promising. Because of this change the customer account entries grid appears odd, with a refunds showing up as a 'Charge'. So, I'm toying with the idea of having entries show up as customer 'Debits' and 'Credits'. I don't know if this will cause user confusion, but we'll play with it for a while and see. It actually reminds me a bit (coming full circle) of the earliest implementations, which kept track of a lease on its own account/ledger, in which credit/debit would be the exact correct terms. git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716@493 97e9348a-65ac-dc4b-aefc-98561f571b83 --- db/schema.sql | 3 +- site/controllers/leases_controller.php | 42 ++++- .../statement_entries_controller.php | 2 +- site/controllers/transactions_controller.php | 50 +++++- site/models/account.php | 4 + site/models/statement_entry.php | 67 ++++---- site/models/transaction.php | 31 ++-- site/views/accounts/collected.ctp | 2 +- site/views/elements/statement_entries.ctp | 5 +- site/views/leases/refund.ctp | 162 +++++++++++------- site/views/statement_entries/view.ctp | 2 +- site/views/transactions/view.ctp | 2 +- 12 files changed, 238 insertions(+), 134 deletions(-) diff --git a/db/schema.sql b/db/schema.sql index 434c952..88f7eee 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -886,7 +886,7 @@ INSERT INTO `pmgr_accounts` (`type`, `name`) ('LIABILITY', 'A/P' ); INSERT INTO `pmgr_accounts` (`type`, `name`, `receipts`, `refunds`) VALUES - ('ASSET', 'Cash', 1, 1), + ('ASSET', 'Cash', 1, 0), ('ASSET', 'Check', 1, 0), ('ASSET', 'Money Order', 1, 0), ('ASSET', 'ACH', 1, 0), @@ -1079,6 +1079,7 @@ CREATE TABLE `pmgr_statement_entries` ( 'REVERSAL', -- Reversal of a charge 'VOUCHER', -- Agreement to pay 'PAYMENT', -- Payment of a Voucher + 'REFUND', -- Payment due to refund 'SURPLUS', -- Surplus Receipt Funds 'WAIVER', -- Waived Charge -- REVISIT : 20090730 diff --git a/site/controllers/leases_controller.php b/site/controllers/leases_controller.php index 75d6474..3187622 100644 --- a/site/controllers/leases_controller.php +++ b/site/controllers/leases_controller.php @@ -239,14 +239,42 @@ class LeasesController extends AppController { */ function refund($id) { - $this->Lease->refund($id); - $this->render('/fake'); -/* // Obtain the overall lease balance */ -/* $stats = $this->Lease->stats($id); */ -/* $outstanding_balance = $stats['balance']; */ + $lease = $this->Lease->find + ('first', array + ('contain' => array + (// Models + 'Unit' => array('fields' => array('id', 'name')), + 'Customer' => array('fields' => array('id', 'name')), + ), -/* $this->set(compact('lease', 'title', */ -/* 'outstanding_balance')); */ + 'conditions' => array(array('Lease.id' => $id), + // Make sure lease is not closed... + array('Lease.close_date' => null), + ), + )); + if (empty($lease)) { + $this->redirect(array('action'=>'view', $id)); + } + + // Determine the lease balance, bailing if the customer owes money + $balance = $this->Lease->balance($id); + if ($balance >= 0) { + $this->redirect(array('action'=>'view', $id)); + } + + // The refund will be for a positive amount + $balance *= -1; + + // Get the accounts capable of paying the refund + $refundAccounts = $this->Lease->StatementEntry->Account->refundAccounts(); + $defaultAccount = current($refundAccounts); + $this->set(compact('refundAccounts', 'defaultAccount')); + + // Prepare to render + $title = ('Lease #' . $lease['Lease']['number'] . ': ' . + $lease['Unit']['name'] . ': ' . + $lease['Customer']['name'] . ': Refund'); + $this->set(compact('title', 'lease', 'balance')); } diff --git a/site/controllers/statement_entries_controller.php b/site/controllers/statement_entries_controller.php index 4366ccb..48ef075 100644 --- a/site/controllers/statement_entries_controller.php +++ b/site/controllers/statement_entries_controller.php @@ -225,7 +225,7 @@ class StatementEntriesController extends AppController { $stats = $this->StatementEntry->stats($id); - if (strtoupper($entry['StatementEntry']['type']) === 'CHARGE') + if (in_array(strtoupper($entry['StatementEntry']['type']), $this->StatementEntry->debitTypes())) $stats = $stats['Charge']; else $stats = $stats['Disbursement']; diff --git a/site/controllers/transactions_controller.php b/site/controllers/transactions_controller.php index 2173e54..4f091a8 100644 --- a/site/controllers/transactions_controller.php +++ b/site/controllers/transactions_controller.php @@ -283,17 +283,61 @@ class TransactionsController extends AppController { if (empty($data['Lease']['id'])) $data['Lease']['id'] = null; + pr(compact('data')); + if (!$this->Transaction->addReceipt($data, $data['Customer']['id'], - $this->data['Lease']['id'])) { + $data['Lease']['id'])) { $this->Session->setFlash("WRITE OFF FAILED", true); // REVISIT 20090706: // Until we can work out the session problems, // just die. - die("

RECEIPT FAILED

"); + die("

WRITE-OFF FAILED

"); + } + + $this->render('/fake'); + + // Return to viewing the lease/customer + if (empty($data['Lease']['id'])) + $this->redirect(array('controller' => 'customers', + 'action' => 'view', + $data['Customer']['id'])); + else + $this->redirect(array('controller' => 'leases', + 'action' => 'view', + $data['Lease']['id'])); + } + + + /************************************************************************** + ************************************************************************** + ************************************************************************** + * action: postRefund + * - handles issuing a customer refund + */ + + function postRefund() { + if (!$this->RequestHandler->isPost()) { + echo('

THIS IS NOT A POST FOR SOME REASON

'); + return; + } + + $data = $this->data; + if (empty($data['Customer']['id'])) + $data['Customer']['id'] = null; + if (empty($data['Lease']['id'])) + $data['Lease']['id'] = null; + + if (!$this->Transaction->addRefund($data, + $data['Customer']['id'], + $data['Lease']['id'])) { + $this->Session->setFlash("REFUND FAILED", true); + // REVISIT 20090706: + // Until we can work out the session problems, + // just die. + die("

REFUND FAILED

"); } - pr(compact('data')); $this->render('/fake'); // Return to viewing the lease/customer diff --git a/site/models/account.php b/site/models/account.php index c841db1..ba55f32 100644 --- a/site/models/account.php +++ b/site/models/account.php @@ -207,6 +207,10 @@ class Account extends AppModel { return $this->relatedAccounts('deposits', array('order' => 'name')); } + function refundAccounts() { + return $this->relatedAccounts('refunds', array('order' => 'name')); + } + /************************************************************************** ************************************************************************** diff --git a/site/models/statement_entry.php b/site/models/statement_entry.php index ef02fdc..754eb32 100644 --- a/site/models/statement_entry.php +++ b/site/models/statement_entry.php @@ -31,13 +31,17 @@ class StatementEntry extends AppModel { */ function debitTypes() { - return array('CHARGE', 'VOUCHER'); + return array('CHARGE', 'PAYMENT', 'REFUND'); } function creditTypes() { return array('DISBURSEMENT', 'WAIVER', 'SURPLUS'); } + function voidTypes() { + return array('VOID'); + } + /************************************************************************** ************************************************************************** @@ -46,32 +50,36 @@ class StatementEntry extends AppModel { */ function chargeDisbursementFields($sum = false, $entry_name = 'StatementEntry') { - $charges = array('CHARGE', 'VOUCHER'); - $nulls = array('PAYMENT', 'VOID'); + $debits = $this->debitTypes(); + $credits = $this->creditTypes(); + $voids = $this->voidTypes(); - foreach ($charges AS &$enum) + foreach ($debits AS &$enum) $enum = "'" . $enum . "'"; - foreach ($nulls AS &$enum) + foreach ($credits AS &$enum) + $enum = "'" . $enum . "'"; + foreach ($voids AS &$enum) $enum = "'" . $enum . "'"; - $charge_set = implode(", ", $charges); - $null_set = implode(", ", $nulls); + $debit_set = implode(", ", $debits); + $credit_set = implode(", ", $credits); + $void_set = implode(", ", $voids); $fields = array ( ($sum ? 'SUM(' : '') . - "IF({$entry_name}.type IN ({$charge_set})," . + "IF({$entry_name}.type IN ({$debit_set})," . " {$entry_name}.amount, NULL)" . ($sum ? ')' : '') . ' AS charge' . ($sum ? 's' : ''), ($sum ? 'SUM(' : '') . - "IF({$entry_name}.type NOT IN({$charge_set}, ${null_set})," . + "IF({$entry_name}.type IN({$credit_set})," . " {$entry_name}.amount, NULL)" . ($sum ? ')' : '') . ' AS disbursement' . ($sum ? 's' : ''), ($sum ? 'SUM(' : '') . - "IF({$entry_name}.type IN ({$null_set}), 0," . - " IF({$entry_name}.type IN ({$charge_set}), 1, -1))" . + "IF({$entry_name}.type IN ({$debit_set}), 1," . + " IF({$entry_name}.type IN ({$credit_set}), -1, 0))" . " * IF({$entry_name}.amount, {$entry_name}.amount, 0)" . ($sum ? ')' : '') . ' AS balance', ); @@ -263,23 +271,16 @@ class StatementEntry extends AppModel { function reconciledSetQuery($set, $query) { $this->queryInit($query); - if ($set == 'CHARGE' || $set == 'DISBURSEMENT') - $query['conditions'][] = array('StatementEntry.type' => $set); + if (in_array($set, $this->debitTypes())) + $query['link']['DisbursementEntry'] = array('fields' => array("SUM(DisbursementEntry.amount) AS reconciled")); + elseif (in_array($set, $this->creditTypes())) + $query['link']['ChargeEntry'] = array('fields' => array("SUM(ChargeEntry.amount) AS reconciled")); else die("INVALID RECONCILE SET"); - if ($set == 'CHARGE') - $query['link']['DisbursementEntry'] = array('fields' => array("SUM(DisbursementEntry.amount) AS reconciled")); - if ($set == 'DISBURSEMENT') - $query['link']['ChargeEntry'] = array('fields' => array("SUM(ChargeEntry.amount) AS reconciled")); - + $query['conditions'][] = array('StatementEntry.type' => $set); $query['group'] = 'StatementEntry.id'; - // REVISIT: TESTING - //$query['link']['DisbursementEntry'] = array('fields' => array("(`DisbursementEntry.amount`+0) AS reconciled")); - //$query['group'] = null; - // END REVISIT - return $query; } @@ -337,9 +338,9 @@ class StatementEntry extends AppModel { $query['conditions'][] = array('StatementEntry.id' => $id); - if ($this->data['StatementEntry']['type'] == 'CHARGE') + if (in_array($this->data['StatementEntry']['type'], $this->debitTypes())) $query['link']['DisbursementEntry'] = array(); - if ($this->data['StatementEntry']['type'] == 'DISBURSEMENT') + if (in_array($this->data['StatementEntry']['type'], $this->creditTypes())) $query['link']['ChargeEntry'] = array(); return $query; @@ -372,7 +373,7 @@ class StatementEntry extends AppModel { $charge_ids = null, $disbursement_type = null, $customer_id = null, $lease_id = null) { - $this->prFunctionLevel(25); + //$this->prFunctionLevel(25); $this->prEnter(compact('query', 'receipt_id', 'charge_ids', 'disbursement_type', 'customer_id', 'lease_id')); @@ -450,15 +451,19 @@ class StatementEntry extends AppModel { $lquery['conditions'][] = array('StatementEntry.lease_id' => $lease_id); } $lquery['order'] = 'StatementEntry.effective_date ASC'; - $charges = $this->reconciledSet('CHARGE', $lquery, true); - $this->pr(18, compact('charges'), - "Outstanding Charges Determined"); + $charges = array(); + foreach ($this->debitTypes() AS $dtype) { + $rset = $this->reconciledSet($dtype, $lquery, true); + $entries = $rset['entries']; + $charges = array_merge($charges, $entries); + $this->pr(18, compact('dtype', 'entries'), "Outstanding Debit Entries"); + } // Initialize our list of used credits $used_credits = array(); // Work through all unpaid charges, applying disbursements as we go - foreach ($charges['entries'] AS $charge) { + foreach ($charges AS $charge) { $this->pr(20, compact('charge'), 'Process Charge'); @@ -627,7 +632,7 @@ class StatementEntry extends AppModel { $charge_query['fields'] = array(); $charge_query['fields'][] = "SUM(StatementEntry.amount) AS total"; - $charge_query['conditions'][] = array('StatementEntry.type' => array('CHARGE', 'VOUCHER')); + $charge_query['conditions'][] = array('StatementEntry.type' => $this->debitTypes()); $result = $this->find('first', $charge_query); $stats['Charge'] = $result[0]; diff --git a/site/models/transaction.php b/site/models/transaction.php index f7df39f..5f3b7ad 100644 --- a/site/models/transaction.php +++ b/site/models/transaction.php @@ -324,28 +324,21 @@ class Transaction extends AppModel { function addRefund($data, $customer_id, $lease_id = null) { $this->prEnter(compact('data', 'customer_id', 'lease_id')); - // REVISIT : 20090804 - // NOT IMPLEMENTED AT ALL. Just cut and paste so far - return array('error' => true); - - // Establish the transaction as a Refund + // 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. $refund =& $data['Transaction']; $refund += - array('type' => 'CREDIT_NOTE', - 'crdr' => 'DEBIT', - 'account_id' => $this->Account->accountReceivableAccountID(), - 'customer_id' => $customer_id, - 'lease_id' => $lease_id, - ); + array('account_id' => $this->Account->accountReceivableAccountID()); - // Go through the statement entries and flag as vouchers + // 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' => 'VOUCHER', - 'crdr' => 'CREDIT', - 'account_id' => $this->Account->accountPayableAccountID(), - ); + $entry += array('type' => 'REFUND'); - $ids = $this->addTransaction($data['Transaction'], $data['Entry']); + $ids = $this->addPayment($data, $customer_id, $lease_id); if (isset($ids['transaction_id'])) $ids['refund_id'] = $ids['transaction_id']; @@ -363,10 +356,6 @@ class Transaction extends AppModel { function addPayment($data, $customer_id, $lease_id = null) { $this->prEnter(compact('data', 'customer_id', 'lease_id')); - // REVISIT : 20090804 - // NOT IMPLEMENTED AT ALL. Just cut and paste so far - return array('error' => true); - // Establish the transaction as an payment $payment =& $data['Transaction']; $payment += diff --git a/site/views/accounts/collected.ctp b/site/views/accounts/collected.ctp index 2a80012..c70768d 100644 --- a/site/views/accounts/collected.ctp +++ b/site/views/accounts/collected.ctp @@ -166,7 +166,7 @@ echo $this->element('statement_entries', array 'caption' => 'Collected ' . Inflector::pluralize($account['name']), 'filter' => array('StatementEntry.type' => 'DISBURSEMENT', 'ChargeEntry.account_id' => $account['id']), - 'exclude' => array('Account', 'Charge'), + 'exclude' => array('Account', 'Charge', 'Type'), ), )); diff --git a/site/views/elements/statement_entries.ctp b/site/views/elements/statement_entries.ctp index 61b9882..997c504 100644 --- a/site/views/elements/statement_entries.ctp +++ b/site/views/elements/statement_entries.ctp @@ -17,8 +17,9 @@ $cols['Unit'] = array('index' => 'Unit.name', 'formatter' => $cols['Comment'] = array('index' => 'StatementEntry.comment', 'formatter' => 'comment', 'width'=>150); -$cols['Charge'] = array('index' => 'charge', 'formatter' => 'currency'); -$cols['Payment'] = array('index' => 'disbursement', 'formatter' => 'currency'); +$cols['Type'] = array('index' => 'StatementEntry.type', 'formatter' => 'enum', 'width'=>120); +$cols['Debit'] = array('index' => 'charge', 'formatter' => 'currency'); +$cols['Credit'] = array('index' => 'disbursement', 'formatter' => 'currency'); $cols['Applied'] = array('index' => "applied", 'formatter' => 'currency'); $cols['Sub-Total'] = array('index' => 'subtotal-balance', 'formatter' => 'currency', 'sortable' => false); diff --git a/site/views/leases/refund.ctp b/site/views/leases/refund.ctp index 31ba0a7..bdf06ed 100644 --- a/site/views/leases/refund.ctp +++ b/site/views/leases/refund.ctp @@ -1,74 +1,106 @@ ' . "\n"; -echo '

Perform Bank Deposit

' . "\n"; -echo '

Make sure to select the checkboxes below for only those types of currency (Cash, Check, etc) which you intend to actually deposit (you can see all the individual items by dropping down the list below the checkbox). Then, select the Deposit Account where you will make the deposit, and click "Perform Deposit" to close the books on the selected currency types and reset them to a zero balance. On the next page, you will be provided with a deposit slip to prepare the actual deposit.' . "\n"; +echo '

' . "\n"; +echo '

Issue Refund

' . "\n"; +echo '

Enter the amount to refund, and the account to pay it from.' . "\n"; echo '


' . "\n"; -//pr(compact('tillableAccount', 'depositableAccount')); - -echo $form->create(null, array('id' => 'deposit-form', - 'url' => array('controller' => 'accounts', - 'action' => 'deposit'))); - -foreach ($tillableAccount AS $acct) { - //$acct = $acct['Account']; - - echo "\n"; - echo $form->input('Tillable.Ledger.'.$acct['CurrentLedger']['id'].'.checked', - array(//'label' => $acct['Account']['name'], - 'type' => 'checkbox', - 'checked' => true, - 'value' => true, - 'label' => (" I have exactly " . - FormatHelper::currency($acct['Account']['stats']['Ledger']['balance']) . - " in " . ($acct['Account']['name'] === 'Cash' - ? 'Cash' - : Inflector::pluralize($acct['Account']['name'])) . - " and will be depositing it all.") - )); - echo "\n"; - echo $form->input('Tillable.Ledger.'.$acct['CurrentLedger']['id'].'.amount', - array('type' => 'hidden', - 'value' => $acct['Account']['stats']['Ledger']['balance'], - )); - echo "\n"; - echo $form->input('Tillable.Ledger.'.$acct['CurrentLedger']['id'].'.account_id', - array('type' => 'hidden', - 'value' => $acct['Account']['id'], - )); - echo "\n"; - echo $form->input('Tillable.Ledger.'.$acct['CurrentLedger']['id'].'.account_name', - array('type' => 'hidden', - 'value' => $acct['Account']['name'], - )); - echo "\n"; - - $grid_div_id = 'ledger_entries'.$acct['CurrentLedger']['id'].'-list'; - echo $this->element('ledger_entries', array - (// Element configuration - 'ledger_id' => $acct['CurrentLedger']['id'], - 'no_account' => true, - - // Grid configuration - 'config' => array - ( - 'grid_div_id' => $grid_div_id, - 'caption' => ('Items in '.$acct['Account']['name'].' Ledger'), - 'grid_setup' => array('hiddengrid' => true), - ), - )); +if (isset($lease)) { + $customer = $lease['Customer']; + $unit = $lease['Unit']; } -$options = array(); -foreach ($depositableAccount AS $acct) { - $options[$acct['Account']['id']] = $acct['Account']['name']; +if (isset($customer['Customer'])) + $customer = $customer['Customer']; + +if (isset($lease['Lease'])) + $lease = $lease['Lease']; + +// We're not actually using a grid to select the customer / lease +// but we could/should be, and the result would be selection-text +echo ('

' . + '' . "\n"); + +echo ('' . + ' ' . + '' . "\n"); + +if (isset($lease)) + echo ('' . + ' ' . + '' . "\n"); + +echo ('' . + ' ' . + '' . "\n"); + +echo ('
' . $customer['name'] . '' . '(Customer #' . $customer['id'] . ')' . '
' . 'Unit ' . $unit['name'] . '' . '(Lease #' . $lease['number'] . ')' . '
Refundable Balance:' . FormatHelper::currency($balance) . '
' . + '
' . "\n"); + + +echo $form->create(null, array('id' => 'refund-form', + 'url' => array('controller' => 'transactions', + 'action' => 'postRefund'))); + + +// REVISIT : 20090805 +// Add Tender information to log specifically _how_ refund was paid. + +echo $this->element('form_table', + array('class' => "item refund transaction entry", + //'with_name_after' => ':', + 'field_prefix' => 'Transaction', + 'fields' => array + ("stamp" => array('opts' => + array('type' => 'text'), + 'between' => 'Now', + ), + "amount" => array('prefix' => 'Entry.0', + 'opts' => + array('value' => + FormatHelper::currency($balance, false, ''), + ), + ), + "account_id" => array('prefix' => 'Entry.0', + 'name' => 'Account', + 'opts' => + array('options' => $refundAccounts, + 'value' => $defaultAccount, + ), + ), + "comment" => array('opts' => array('size' => 50), + ), + ))) . "\n"; + +echo $form->input("Customer.id", + array('type' => 'hidden', + 'value' => $customer['id'])) . "\n"; + +if (isset($lease['id'])) + echo $form->input("Lease.id", + array('type' => 'hidden', + 'value' => $lease['id'])) . "\n"; + +echo $form->end('Issue Refund'); +?> + + + +
diff --git a/site/views/statement_entries/view.ctp b/site/views/statement_entries/view.ctp index 8c59c84..d7f677e 100644 --- a/site/views/statement_entries/view.ctp +++ b/site/views/statement_entries/view.ctp @@ -63,7 +63,7 @@ if (strtoupper($entry['type']) === 'CHARGE') { //$remaining_caption = "Charge Balance"; } else { - $applied_caption = "Dispursed to Charges"; + $applied_caption = "Disbursed to Charges"; //$remaining_caption = "Disbursement Balance"; } diff --git a/site/views/transactions/view.ctp b/site/views/transactions/view.ctp index 2f0b62f..5099bd9 100644 --- a/site/views/transactions/view.ctp +++ b/site/views/transactions/view.ctp @@ -69,7 +69,7 @@ echo '
' . "\n"; if ($transaction['type'] === 'INVOICE' || $transaction['type'] === 'RECEIPT' || - $transaction['type'] === 'CREDIT_NOTE' + $transaction['type'] === 'PAYMENT' ) { echo $this->element('statement_entries', array (// Grid configuration