Several changes in an effort to support charge reversals. I can't imagine this is all working flawlessly, as I'm not quite sure how it even _should_ work.

git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716@490 97e9348a-65ac-dc4b-aefc-98561f571b83
This commit is contained in:
abijah
2009-08-05 07:54:57 +00:00
parent 5247bb8db6
commit cca698d437
6 changed files with 236 additions and 220 deletions

View File

@@ -961,10 +961,15 @@ DROP TABLE IF EXISTS `pmgr_transactions`;
CREATE TABLE `pmgr_transactions` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`type` ENUM('INVOICE',
'RECEIPT',
'VOUCHER',
'DEPOSIT', -- Probably should use VOUCHER for DEPOSIT
-- REVISIT <AP>: 20090804
-- I'm not sure about most of these terms.
-- Just as long as they're distinct though... I can rename them later
`type` ENUM('INVOICE', -- Sales Invoice
'RECEIPT', -- Actual receipt of monies
'PURCHASE_INVOICE', -- Committment to pay
'CREDIT_NOTE', -- Inverse of Sales Invoice
'PAYMENT', -- Actual payment
'DEPOSIT',
'CLOSE', -- Essentially an internal (not accounting) transaction
-- 'CREDIT',
-- 'REFUND',
@@ -1066,11 +1071,16 @@ DROP TABLE IF EXISTS `pmgr_statement_entries`;
CREATE TABLE `pmgr_statement_entries` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`type` ENUM('CHARGE',
'DISBURSEMENT',
'VOUCHER-PAYMENT', -- REVISIT: Make PAYMENT
'SURPLUS',
'WAIVER',
-- REVISIT <AP>: 20090804
-- I'm not sure about most of these terms.
-- Just as long as they're distinct though... I can rename them later
`type` ENUM('CHARGE', -- Invoiced Charge to Customer
'DISBURSEMENT', -- Disbursement of Receipt Funds
'REVERSAL', -- Reversal of a charge
'VOUCHER', -- Agreement to pay
'PAYMENT', -- Payment of a Voucher
'SURPLUS', -- Surplus Receipt Funds
'WAIVER', -- Waived Charge
-- REVISIT <AP>: 20090730
-- VOID is used for handling NSF and perhaps charge reversals.
-- It's not clear this is the best way to handle these things.

View File

@@ -191,7 +191,7 @@ class StatementEntriesController extends AppController {
function waive($id) {
$this->StatementEntry->waive($id);
//$this->redirect(array('action'=>'view', $id));
$this->redirect(array('action'=>'view', $id));
}
@@ -223,44 +223,6 @@ class StatementEntriesController extends AppController {
$reconciled = $this->StatementEntry->reconciledEntries($id);
/* // REVISIT <AP>: 20090711 */
/* // It's not clear whether we should be able to reverse charges that have */
/* // already been paid/cleared/reconciled. Certainly, that will be the */
/* // case when someone has pre-paid and then moves out early. However, this */
/* // will work well for items accidentally charged but not yet paid for. */
/* if ((!$entry['DebitLedger']['Account']['trackable'] || */
/* $stats['debit']['amount_reconciled'] == 0) && */
/* (!$entry['CreditLedger']['Account']['trackable'] || */
/* $stats['credit']['amount_reconciled'] == 0) */
/* && 0 */
/* ) */
/* { */
/* // Set up dynamic menu items */
/* $this->sidemenu_links[] = */
/* array('name' => 'Operations', 'header' => true); */
/* $this->sidemenu_links[] = */
/* array('name' => 'Undo', */
/* 'url' => array('action' => 'reverse', */
/* $id)); */
/* } */
/* if ($this->StatementEntry->Ledger->Account->type */
/* ($entry['CreditLedger']['Account']['id']) == 'INCOME') */
/* { */
/* // Set up dynamic menu items */
/* $this->sidemenu_links[] = */
/* array('name' => 'Operations', 'header' => true); */
/* $this->sidemenu_links[] = */
/* array('name' => 'Reverse', */
/* 'url' => array('action' => 'reverse', */
/* $id)); */
/* } */
$stats = $this->StatementEntry->stats($id);
if (strtoupper($entry['StatementEntry']['type']) === 'CHARGE')
@@ -269,16 +231,21 @@ class StatementEntriesController extends AppController {
$stats = $stats['Disbursement'];
if (strtoupper($entry['StatementEntry']['type']) === 'CHARGE' &&
$stats['balance'] > 0) {
if (strtoupper($entry['StatementEntry']['type']) === 'CHARGE') {
// Set up dynamic menu items
$this->sidemenu_links[] =
array('name' => 'Operations', 'header' => true);
$this->sidemenu_links[] =
array('name' => 'Waive Balance',
'url' => array('action' => 'waive',
array('name' => 'Reverse',
'url' => array('action' => 'reverse',
$id));
if ($stats['balance'] > 0)
$this->sidemenu_links[] =
array('name' => 'Waive Balance',
'url' => array('action' => 'waive',
$id));
}
// Prepare to render.

View File

@@ -22,7 +22,7 @@ class StatementEntry extends AppModel {
);
var $default_log_level = array('log' => 30, 'show' => 15);
//var $default_log_level = array('log' => 30, 'show' => 15);
/**************************************************************************
**************************************************************************
@@ -40,15 +40,12 @@ class StatementEntry extends AppModel {
($sum ? ')' : '') . ' AS charge' . ($sum ? 's' : ''),
($sum ? 'SUM(' : '') .
//"IF({$entry_name}.type IN('DISBURSEMENT', 'SURPLUS', 'WAIVER')," .
"IF({$entry_name}.type NOT IN('CHARGE', 'VOID')," .
"IF({$entry_name}.type NOT IN('CHARGE', 'PAYMENT', 'VOID')," .
" {$entry_name}.amount, NULL)" .
($sum ? ')' : '') . ' AS disbursement' . ($sum ? 's' : ''),
($sum ? 'SUM(' : '') .
//"IF({$entry_name}.type = 'CHARGE', 1," .
//" IF({$entry_name}.type IN('DISBURSEMENT', 'SURPLUS', 'WAIVER'), -1, 0))" .
"IF({$entry_name}.type = 'VOID', 0," .
"IF({$entry_name}.type IN ('VOID', 'PAYMENT'), 0," .
" IF({$entry_name}.type = 'CHARGE', 1, -1))" .
" * IF({$entry_name}.amount, {$entry_name}.amount, 0)" .
($sum ? ')' : '') . ' AS balance',
@@ -154,110 +151,80 @@ class StatementEntry extends AppModel {
* function: reverse
* - Reverses the charges
*
* SAMPLE MOVE IN w/ PRE DISBURSEMENT
* DEPOSIT RENT A/R RECEIPT CHECK PETTY BANK
* ------- ------- ------- ------- ------- ------- -------
* |25 | 25| | | | |
* | |20 20| | | | |
* | |20 20| | | | |
* | |20 20| | | | |
* | | |25 25| | | |
* | | |20 20| | | |
* | | |20 20| | | |
* | | |20 20| | | |
* | | | |85 85| | |
* | | | | |85 | 85|
* MOVE OUT and REFUND FINAL MONTH
* DEPOSIT RENT C/P RECEIPT CHECK PETTY BANK
* ------- ------- ------- ------- ------- ------- -------
* 25| | |25 | | | | t20 e20a
* | 20| |20 | | | | t20 e20b
* -ONE REFUND CHECK-
* | | 25| |25 | | | t30 e30a
* | | 20| |20 | | | t30 e30b
* | | | 45| | | |45 t40 e40
* -OR MULTIPLE-
* | | 15| |15 | | | t50a e50a
* | | | 15| | |15 | t60a e60a
* | | 30| |30 | | | t50b e50b
* | | | 30| | | |30 t60b e60b
* | | | | | | |
OPTION 1
* |-25 | -25| | | | |
* | |-20 -20| | | | |
* | | |-25 -25| | | |
* | | |-20 -20| | | |
OPTION 2
* |-25 | | -25| | | |
* | |-20 | -20| | | |
* | | | |-15 | -15| |
* | | | |-30 | | -30|
* | | | | | | |
*
*/
function reverse($ledger_entries, $stamp = null) {
$this->prEnter(compact('ledger_entries', 'stamp'));
function reverse($id, $stamp = null) {
$this->prEnter(compact('id', 'stamp'));
// If the user only wants to reverse one ID, we'll allow it
if (!is_array($ledger_entries))
$ledger_entries = $this->find
('all', array
('contain' => false,
'conditions' => array('Entry.id' => $ledger_entries)));
$ret = array();
$A = new Account();
// Get the basic information about the entry to be reversed.
$this->recursive = -1;
$charge = $this->read(null, $id);
$charge = $charge['StatementEntry'];
$ar_account_id = $A->accountReceivableAccountID();
$receipt_account_id = $A->receiptAccountID();
$voided_entry_transactions = array();
$reconciled = $this->reconciledEntries($id);
$this->pr(21, compact('reconciled'));
$transaction_id = null;
foreach ($ledger_entries AS $entry) {
$entry = $entry['Entry'];
$amount = -1*$entry['amount'];
if ($reconciled) {
foreach ($reconciled['entries'] AS $entry) {
$voided_entry_transactions[$entry['DisbursementEntry']['transaction_id']]
= array_intersect_key($entry['DisbursementEntry'],
array('customer_id'=>1, 'lease_id'=>1));
if (isset($entry['credit_account_id']))
$refund_account_id = $entry['credit_account_id'];
elseif (isset($entry['CreditLedger']['Account']['id']))
$refund_account_id = $entry['CreditLedger']['Account']['id'];
elseif (isset($entry['credit_ledger_id']))
$refund_account_id = $this->Ledger->accountID($entry['credit_ledger_id']);
else
return $this->prReturn(null);
$this->del($entry['DisbursementEntry']['id']);
continue;
// post new refund in the income account
$ids = $A->postEntry
(array('transaction_id' => $transaction_id),
null,
array('debit_ledger_id' => $A->currentLedgerID($ar_account_id),
'credit_ledger_id' => $A->currentLedgerID($refund_account_id),
'effective_date' => $entry['effective_date'],
'through_date' => $entry['through_date'],
'amount' => $amount,
'lease_id' => $entry['lease_id'],
'customer_id' => $entry['customer_id'],
'comment' => "Refund; Entry #{$entry['id']}",
),
array('debit' => array
(array('Entry' =>
array('id' => $entry['id'],
'amount' => $amount))),
)
);
if ($ids['error'])
return $this->prReturn(null);
$transaction_id = $ids['transaction_id'];
$this->pr(15, compact('ids', 'amount', 'refund_account_id', 'ar_account_id'),
'Posted Refund Ledger Entry');
$DE = new StatementEntry();
$DE->id = $entry['DisbursementEntry']['id'];
$DE->saveField('type', 'VOID');
$DE->saveField('charge_entry_id', null);
}
$this->pr(17, compact('voided_entry_transactions'));
}
return $this->prReturn(true);
// Query the stats to get the remaining balance
$stats = $this->stats($id);
// Build a transaction
$reversal = array('Transaction' => array(), 'Entry' => array());
$reversal['Transaction']['stamp'] = $stamp;
$reversal['Transaction']['comment'] = "Credit Note: Charge Reversal";
if ($charge['type'] !== 'CHARGE')
die("INTERNAL ERROR: REVERSAL ITEM IS NOT CHARGE");
// Add the charge reversal
$reversal['Entry'][] =
array('amount' => $stats['Charge']['total'],
'account_id' => $charge['account_id'],
'comment' => 'Charge Reversal',
);
// Record the reversal transaction
$result = $this->Transaction->addReversal
($reversal, $id, $charge['customer_id'], $charge['lease_id']);
$this->pr(21, compact('result'));
$ret['reversal'] = $result;
if ($result['error'])
$ret['error'] = true;
foreach ($voided_entry_transactions AS $transaction_id => $tx) {
$result = $this->assignCredits
(null,
$transaction_id,
null,
null,
$tx['customer_id'],
$tx['lease_id']
);
$this->pr(21, compact('result'));
$ret['assigned'][] = $result;
if ($result['error'])
$ret['error'] = true;
}
return $this->prReturn($ret + array('error' => false));
}
@@ -421,23 +388,27 @@ OPTION 2
// Next, establish credit from the newly added receipt
$receipt_credit = null;
if (!empty($receipt_id)) {
$lquery = $query;
$lquery['link'] = array('StatementEntry' => $lquery['link']);
$lquery['link'] += array('LedgerEntry' =>
array('conditions' =>
//array(LedgerEntry.'crdr'=>'DEBIT'),
array('LedgerEntry.account_id !=' => $this->Account->accountReceivableAccountID()),
));
$lquery['fields'] = array('Transaction.id', 'Transaction.stamp', 'Transaction.amount',
'LedgerEntry.account_id');
// Very specific case here... no extra conditions
unset($lquery['conditions']);
$this->Transaction->id = $receipt_id;
$lquery =
array('link' =>
array('StatementEntry',
'LedgerEntry' =>
array('conditions' =>
array('LedgerEntry.account_id !=' =>
$this->Account->accountReceivableAccountID()),
),
),
'conditions' => array('Transaction.id' => $receipt_id),
'fields' => array('Transaction.id', 'Transaction.stamp', 'Transaction.amount'),
);
$receipt_credit = $this->Transaction->find('first', $lquery);
if (!$receipt_credit)
die("INTERNAL ERROR: UNABLE TO LOCATE RECEIPT");
$receipt_credit['balance'] = $receipt_credit['Transaction']['amount'];
//$reconciled = $this->reconciledEntries($id);
$stats = $this->Transaction->stats($receipt_id);
$receipt_credit['balance'] =
$receipt_credit['Transaction']['amount'] - $stats['Disbursement']['total'];
$this->pr(18, compact('receipt_credit'),
"Receipt Credit Added");

View File

@@ -139,6 +139,69 @@ class Transaction extends AppModel {
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: addReversal
* - Adds a new charge reversal
*/
function addReversal($data, $charge_id, $customer_id, $lease_id = null) {
$this->prEnter(compact('data', 'charge_id', 'customer_id', 'lease_id'));
if (count($data['Entry']) != 1)
die("INTERNAL ERROR: Should be one Entry for addWaiver");
$data['Transaction']['type'] = 'CREDIT_NOTE';
$data['Transaction']['charge_entry_id'] = $charge_id;
//$data['Entry'][0]['amount'] *= -1;
//$data['Entry'][0]['type'] = 'DISBURSEMENT';
$ids = $this->addReceipt($data, $customer_id, $lease_id);
if (isset($ids['transaction_id']))
$ids['reversal_id'] = $ids['transaction_id'];
/* $new_charge_id = $ids['entries'][0]['StatementEntry']['statement_entry_id']; */
/* $this->StatementEntry->id = $new_charge_id; */
/* $this->StatementEntry->saveField('charge_entry_id', $charge_id); */
return $this->prReturn($ids);
// Just make sure the transaction is marked as an INVOICE
// and that it goes to cover the specific charge.
//$data['Transaction']['type'] = 'INVOICE';
$data['Transaction']['charge_entry_id'] = $charge_id;
// In all other respects this works just like a receipt.
$ids = $this->addReceipt($data, $customer_id, $lease_id);
if (isset($ids['transaction_id']))
$ids['reversal_id'] = $ids['transaction_id'];
return $this->prReturn($ids);
// Establish the transaction as an Invoice Reversal
$reversal =& $data['Transaction'];
$reversal +=
array('type' => 'INVOICE', //'CREDIT_NOTE',
'crdr' => 'CREDIT',
'account_id' => $this->Account->accountReceivableAccountID(),
'customer_id' => $customer_id,
'lease_id' => $lease_id,
);
// Go through the statement entries and flag as reversals
foreach ($data['Entry'] AS &$entry)
$entry += array('type' => 'CHARGE', //'REVERSAL',
'crdr' => 'DEBIT',
);
$ids = $this->addTransaction($data['Transaction'], $data['Entry']);
if (isset($ids['transaction_id']))
$ids['reversal_id'] = $ids['transaction_id'];
return $this->prReturn($ids);
}
/**************************************************************************
**************************************************************************
**************************************************************************
@@ -251,49 +314,11 @@ class Transaction extends AppModel {
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: addVoucher
* - Adds a new voucher transaction, which is money outflow
*/
function addVoucher($data) {
$this->prEnter(compact('data', 'customer_id', 'lease_id'));
// REVISIT <AP>: 20090804
// NOT IMPLEMENTED AT ALL. Just cut and paste so far
return array('error' => true);
// Establish the transaction as an voucher
$voucher =& $data['Transaction'];
$voucher +=
array('type' => 'VOUCHER',
'crdr' => 'DEBIT',
'account_id' => $this->Account->accountPayableAccountID(),
'customer_id' => null,
'lease_id' => null,
);
// Go through the statement entries and flag as charges
foreach ($data['Entry'] AS &$entry)
$entry += array('type' => 'CHARGE',
'crdr' => 'CREDIT',
);
$ids = $this->addTransaction($data['Transaction'], $data['Entry']);
if (isset($ids['transaction_id']))
$ids['voucher_id'] = $ids['transaction_id'];
return $this->prReturn($ids);
}
/**************************************************************************
**************************************************************************
**************************************************************************
* function: addRefund
* - Adds a new refund transaction
* - Adds a new refund
*/
function addRefund($data, $customer_id, $lease_id = null) {
@@ -303,20 +328,21 @@ class Transaction extends AppModel {
// NOT IMPLEMENTED AT ALL. Just cut and paste so far
return array('error' => true);
// Establish the transaction as an refund
// Establish the transaction as a Refund
$refund =& $data['Transaction'];
$refund +=
array('type' => 'VOUCHER',
'crdr' => 'DEBIT',
'account_id' => $this->Account->accountReceivableAccountID(),
array('type' => 'CREDIT_NOTE',
'crdr' => 'CREDIT',
'account_id' => $this->Account->accountPayableAccountID(),
'customer_id' => $customer_id,
'lease_id' => $lease_id,
);
// Go through the statement entries and flag as charges
// Go through the statement entries and flag as vouchers
foreach ($data['Entry'] AS &$entry)
$entry += array('type' => 'CHARGE',
'crdr' => 'CREDIT',
$entry += array('type' => 'VOUCHER',
'crdr' => 'DEBIT',
'account_id' => $this->Account->accountReceivableAccountID(),
);
$ids = $this->addTransaction($data['Transaction'], $data['Entry']);
@@ -327,6 +353,44 @@ class Transaction extends AppModel {
}
/**************************************************************************
**************************************************************************
**************************************************************************
* 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'));
// REVISIT <AP>: 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 +=
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',
'crdr' => 'CREDIT',
);
$ids = $this->addTransaction($data['Transaction'], $data['Entry']);
if (isset($ids['transaction_id']))
$ids['payment_id'] = $ids['transaction_id'];
return $this->prReturn($ids);
}
/**************************************************************************
**************************************************************************
**************************************************************************
@@ -511,7 +575,9 @@ class Transaction extends AppModel {
// (DISBURSEMENTS will have statement entries created below, when
// assigning credits, and DEPOSITS don't have statement entries)
if ($transaction['type'] != 'INVOICE' && $subtype !== 'NSF')
if (($transaction['account_id'] != $this->Account->accountReceivableAccountID() &&
$transaction['account_id'] != $this->Account->nsfAccountID()) ||
$transaction['crdr'] != 'DEBIT')
$se = null;
// NSF transactions don't use LedgerEntries
@@ -588,15 +654,14 @@ class Transaction extends AppModel {
}
}
if (($transaction['type'] == 'INVOICE' ||
$transaction['type'] == 'RECEIPT') &&
$subtype !== 'NSF' && !$ret['error']) {
if ($transaction['account_id'] == $this->Account->accountReceivableAccountID()
&& !$ret['error']) {
$result = $this->StatementEntry->assignCredits
(null,
($transaction['type'] == 'RECEIPT'
($transaction['crdr'] == 'CREDIT'
? $ret['transaction_id']
: null),
($transaction['type'] == 'RECEIPT' && !empty($transaction['charge_entry_id'])
($transaction['crdr'] == 'CREDIT' && !empty($transaction['charge_entry_id'])
? $transaction['charge_entry_id']
: null),
(!empty($transaction['disbursement_type'])

View File

@@ -25,7 +25,7 @@ $rows[] = array($Ttype, $html->link('#'.$transaction['id'],
$transaction['id'])));
$rows[] = array('Timestamp', FormatHelper::datetime($transaction['stamp']));
$rows[] = array('Effective', FormatHelper::date($entry['effective_date']));
if (in_array($entry['type'], array('CHARGE', /*REVISIT 'VOUCHER-PAYMENT'*/)))
if (in_array($entry['type'], array('CHARGE', 'PAYMENT')))
$rows[] = array('Through', FormatHelper::date($entry['through_date']));
$rows[] = array('Type', $entry['type']);
$rows[] = array('Amount', FormatHelper::currency($entry['amount']));

View File

@@ -67,7 +67,10 @@ echo '<div CLASS="detail supporting">' . "\n";
* Statement Entries
*/
if ($transaction['type'] === 'INVOICE' || $transaction['type'] === 'RECEIPT') {
if ($transaction['type'] === 'INVOICE' ||
$transaction['type'] === 'RECEIPT' ||
$transaction['type'] === 'CREDIT_NOTE'
) {
echo $this->element('statement_entries', array
(// Grid configuration
'config' => array