First pass implementation at generating an invoice, which seems to be working. Largely untested, but worth checking in. Next is to get receipts using the same algorithm, and after that will be to work on a reconciling mechanism, creating payments, and matching them to charges. This checkin includes an additional customer_id fields as part of the transactions table. I think it was an oversight not to be there, as we need some way to keep track of monies which have been paid by a customer, yet not applied. If there is a different way to do it (short of actually having each ledger entry hold customer_id), it escapes me at the moment.
git-svn-id: file:///svn-source/pmgr/branches/yafr_20090716@372 97e9348a-65ac-dc4b-aefc-98561f571b83
This commit is contained in:
@@ -973,6 +973,22 @@ CREATE TABLE `pmgr_transactions` (
|
||||
|
||||
`stamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- All entries of a transaction should be for the same
|
||||
-- customer. By keeping track of customer here, it ensures
|
||||
-- that we can always track what's happening with the user
|
||||
-- even if there are only ledger entries, and no statement
|
||||
-- entries for some reason (the primary concern being the
|
||||
-- receipt of money, with nothing to pay for).
|
||||
-- customer_id can be NULL, for internal transfers and such.
|
||||
-- In that case, there should be no statement entries for
|
||||
-- this transaction, only ledger entries.
|
||||
-- REVISIT <AP>: 20090723
|
||||
-- It sounds like a transaction that has customer_id as NULL
|
||||
-- is really a fundamentally different type of "transaction".
|
||||
-- Do we need to have a new table for those type of
|
||||
-- entries / activities?
|
||||
`customer_id` INT(10) UNSIGNED DEFAULT NULL,
|
||||
|
||||
-- The account/ledger of the transaction set
|
||||
-- (e.g. A/R, Bank, etc)
|
||||
`account_id` INT(10) UNSIGNED NOT NULL,
|
||||
@@ -1072,6 +1088,9 @@ CREATE TABLE `pmgr_statement_entries` (
|
||||
`through_date` DATE DEFAULT NULL, -- last day
|
||||
`due_date` DATE DEFAULT NULL,
|
||||
|
||||
-- Customer ID is redundant, since it is saved as part of the
|
||||
-- transaction. Keeping it here anyway, for simplicity. If it's
|
||||
-- truly redundant, and unnecessary, we can always re
|
||||
`customer_id` INT(10) UNSIGNED NOT NULL,
|
||||
`lease_id` INT(10) UNSIGNED NOT NULL,
|
||||
|
||||
|
||||
@@ -915,6 +915,7 @@ foreach $row (@{query($sdbh, $query)}) {
|
||||
addRow('transactions', {
|
||||
'type' => 'INVOICE',
|
||||
'stamp' => $stamp,
|
||||
'customer_id' => $newdb{'lookup'}{'ledger'}{$row->{'LedgerID'}}{'customer_id'},
|
||||
'account_id' => $newdb{'lookup'}{'account'}{'A/R'}{'account_id'},
|
||||
'ledger_id' => $newdb{'lookup'}{'account'}{'A/R'}{'ledger_id'},
|
||||
'crdr' => 'DEBIT',
|
||||
@@ -1109,6 +1110,7 @@ foreach $row (@{query($sdbh, $query)}) {
|
||||
addRow('transactions', {
|
||||
'type' => 'RECEIPT',
|
||||
'stamp' => $stamp,
|
||||
'customer_id' => undef, # Must be set later
|
||||
'account_id' => $newdb{'lookup'}{'account'}{'A/R'}{'account_id'},
|
||||
'ledger_id' => $newdb{'lookup'}{'account'}{'A/R'}{'ledger_id'},
|
||||
'crdr' => 'CREDIT',
|
||||
@@ -1200,7 +1202,12 @@ foreach $row (@{query($sdbh, $query)})
|
||||
'account_id' => $newdb{'lookup'}{'receipt'}{$row->{'ReceiptNum'}}{$row->{'PaymentType'}}{'credit_account_id'},
|
||||
};
|
||||
|
||||
# Update the receipt customer_id, now that we have payment info
|
||||
$newdb{'tables'}{'transactions'}{'rows'}[
|
||||
$newdb{'lookup'}{'payment'}{$row->{'PaymentID'}}{'receipt_id'}
|
||||
]{'customer_id'} = $newdb{'lookup'}{'payment'}{$row->{'PaymentID'}}{'customer_id'};
|
||||
|
||||
# Use the Memo as our comment, if it exists
|
||||
my $comment = $row->{'Memo'} || "Payment: $row->{'ReceiptNum'}; Type: $row->{'PaymentType'}";
|
||||
|
||||
# Add the Payment Statement Entry
|
||||
|
||||
@@ -20,42 +20,23 @@ class Transaction extends AppModel {
|
||||
*/
|
||||
|
||||
function addInvoice($data, $customer_id, $lease_id = null) {
|
||||
// Create some models for convenience
|
||||
$A = new Account();
|
||||
|
||||
//pr(compact('data', 'customer_id', 'lease_id'));
|
||||
|
||||
// Assume this will succeed
|
||||
$ret = true;
|
||||
|
||||
// Establish the key invoice parameters
|
||||
$invoice = array_intersect_key($data, array('Invoice'=>1));
|
||||
// Establish the transaction as an invoice
|
||||
$invoice =& $data['Transaction'];
|
||||
$invoice['type'] = 'INVOICE';
|
||||
$invoice['crdr'] = 'DEBIT';
|
||||
$invoice['account_id'] = $this->Account->accountReceivableAccountID();
|
||||
|
||||
// Determine the total charges on the invoice
|
||||
$invoice['amount'] = 0;
|
||||
foreach ($data['LedgerEntry'] AS $entry)
|
||||
$invoice['amount'] += $entry['amount'];
|
||||
|
||||
// Go through the entered charges
|
||||
foreach ($data['LedgerEntry'] AS $entry) {
|
||||
//pr(compact('entry'));
|
||||
// Create the receipt entry, and reconcile the credit side
|
||||
// of the double-entry (which should be A/R) as a payment.
|
||||
$ids = $this->LedgerEntry->Ledger->Account->postLedgerEntry
|
||||
($invoice,
|
||||
array('debit_ledger_id' => $A->currentLedgerID($A->accountReceivableAccountID()),
|
||||
'credit_ledger_id' => $A->currentLedgerID($entry['account_id'])
|
||||
) + $entry
|
||||
);
|
||||
|
||||
if ($ids['error'])
|
||||
$ret = false;
|
||||
|
||||
$invoice = array_intersect_key($ids, array('invoice_id'=>1));
|
||||
// Go through the statement entries and flag as charges
|
||||
foreach ($data['Entry'] AS &$entry) {
|
||||
$entry['type'] = 'CHARGE';
|
||||
$entry['crdr'] = 'CREDIT';
|
||||
}
|
||||
|
||||
return $ret;
|
||||
$ids = $this->addTransaction($data, $customer_id, $lease_id);
|
||||
if (isset($ids['transaction_id']))
|
||||
$ids['invoice_id'] = $ids['transaction_id'];
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,43 +48,293 @@ class Transaction extends AppModel {
|
||||
*/
|
||||
|
||||
function addReceipt($data, $customer_id, $lease_id = null) {
|
||||
// Create some models for convenience
|
||||
$A = new Account();
|
||||
|
||||
// Assume this will succeed
|
||||
$ret = true;
|
||||
|
||||
// Establish the key receipt parameters
|
||||
$receipt = array_intersect_key($data, array('stamp'=>1, 'type'=>1, 'name'=>1, 'amount'=>1,
|
||||
'data1'=>1, 'data2'=>1, 'data3'=>1, 'data4'=>1));
|
||||
// Establish the transaction as an receipt
|
||||
$receipt =& $data['Transaction'];
|
||||
$receipt['type'] = 'RECEIPT';
|
||||
$receipt['crdr'] = 'CREDIT';
|
||||
$receipt['account_id'] = $this->Account->accountReceivableAccountID();
|
||||
|
||||
// Determine the total charges on the receipt
|
||||
$receipt['amount'] = 0;
|
||||
foreach ($data['LedgerEntry'] AS $entry)
|
||||
$receipt['amount'] += $entry['amount'];
|
||||
// Go through the statement entries and flag as payments
|
||||
foreach ($data['Entry'] AS &$entry) {
|
||||
$entry['type'] = 'PAYMENT';
|
||||
$entry['crdr'] = 'DEBIT';
|
||||
}
|
||||
|
||||
$ids = $this->addTransaction($data, $customer_id, $lease_id);
|
||||
if (isset($ids['transaction_id']))
|
||||
$ids['receipt_id'] = $ids['transaction_id'];
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
|
||||
/**************************************************************************
|
||||
**************************************************************************
|
||||
**************************************************************************
|
||||
* 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.
|
||||
*
|
||||
* $data
|
||||
* - Transaction
|
||||
* - [MANDATORY]
|
||||
* - type (INVOICE, RECEIPT)
|
||||
* - account_id
|
||||
* - crdr
|
||||
* - [OPTIONAL]
|
||||
* - stamp
|
||||
* (default: NOW)
|
||||
* - comment
|
||||
* - [AUTOMATICALLY SET] (if set, these items will be overwritten)
|
||||
* - id
|
||||
* - amount
|
||||
* - customer_id
|
||||
* - ledger_id
|
||||
*
|
||||
* - Entry (array)
|
||||
* - [MANDATORY]
|
||||
* - type (CHARGE, PAYMENT)
|
||||
* - account_id
|
||||
* - crdr
|
||||
* - amount
|
||||
* - [OPTIONAL]
|
||||
* - effective_date
|
||||
* - through_date
|
||||
* - due_date
|
||||
* - comment (used for statement or ledger entry, based on context)
|
||||
* - ledger_entry_comment
|
||||
* - statement_entry_comment
|
||||
* - Tender
|
||||
* - [MANDATORY]
|
||||
* - type (CASH, CHECK, etc)
|
||||
* - [OPTIONAL]
|
||||
* - name
|
||||
* (default: Entry Account Name & data1)
|
||||
* - data1, data2, data3, data4
|
||||
* - comment
|
||||
* - [AUTOMATICALLY SET] (if set, these items will be overwritten)
|
||||
* - id
|
||||
* - ledger_entry_id
|
||||
* - deposit_transaction_id
|
||||
* - nsf_transaction_id
|
||||
* - [AUTOMATICALLY SET] (if set, these items will be overwritten)
|
||||
* - id
|
||||
* - transaction_id
|
||||
* - ledger_id
|
||||
*
|
||||
*/
|
||||
|
||||
function addTransaction($data, $customer_id, $lease_id = null) {
|
||||
pr(compact('data', 'customer_id', 'lease_id'));
|
||||
|
||||
// Automatically figure out the customer if we have the lease
|
||||
if (!empty($lease_id) && empty($customer_id)) {
|
||||
$L = new Lease();
|
||||
$L->recursive = -1;
|
||||
$lease = $L->read(null, $lease_id);
|
||||
$customer_id = $lease['Lease']['customer_id'];
|
||||
}
|
||||
|
||||
if (!isset($data['Transaction']) || !isset($data['Entry']))
|
||||
return array('error' => true);
|
||||
|
||||
// Keep only relevent items from Transaction
|
||||
$transaction = array_intersect_key($data['Transaction'],
|
||||
array_flip(array('type', 'account_id', 'crdr',
|
||||
'stamp', 'comment',
|
||||
'LedgerEntry')));
|
||||
|
||||
// Verify required Transaction data is present
|
||||
if (empty($transaction['type']) ||
|
||||
empty($transaction['account_id']) ||
|
||||
empty($transaction['crdr']) ||
|
||||
(in_array($transaction['type'], array('INVOICE', 'RECEIPT'))
|
||||
&& !$customer_id) ||
|
||||
(in_array($transaction['type'], array('INVOICE'))
|
||||
&& !$lease_id)
|
||||
) {
|
||||
pr("Transaction verification failed");
|
||||
return array('error' => true);
|
||||
}
|
||||
|
||||
// Verify each Entry, and calculation the transaction total
|
||||
$transaction['amount'] = 0;
|
||||
foreach ($data['Entry'] AS &$entry) {
|
||||
if (empty($entry['type']) ||
|
||||
empty($entry['account_id']) ||
|
||||
empty($entry['crdr']) ||
|
||||
empty($entry['amount']) ||
|
||||
(isset($entry['Tender']) &&
|
||||
(empty($entry['Tender']['type'])
|
||||
))
|
||||
) {
|
||||
pr("Entry verification failed");
|
||||
return array('error' => true);
|
||||
}
|
||||
|
||||
// Keep only relevent items from Entry
|
||||
$entry =
|
||||
array_intersect_key($entry,
|
||||
array_flip(array('type', 'account_id', 'crdr', 'amount',
|
||||
'effective_date', 'through_date', 'due_date',
|
||||
'comment',
|
||||
'ledger_entry_comment',
|
||||
'statement_entry_comment',
|
||||
'Tender')));
|
||||
|
||||
if (isset($entry['Tender'])) {
|
||||
|
||||
// Verify required Tender data is present
|
||||
if (empty($entry['Tender']['type'])) {
|
||||
pr("Tender verification failed");
|
||||
return array('error' => true);
|
||||
}
|
||||
|
||||
// Keep only relevent items
|
||||
$entry['Tender'] =
|
||||
array_intersect_key($entry['Tender'],
|
||||
array_flip(array('type', 'name',
|
||||
'data1', 'data2', 'data3', 'data4',
|
||||
'comment')));
|
||||
}
|
||||
|
||||
// Try to figure out where 'comment' should go
|
||||
if (empty($entry['ledger_entry_comment']) &&
|
||||
!empty($entry['comment']) &&
|
||||
$entry['type'] === 'PAYMENT')
|
||||
$entry['ledger_entry_comment'] = $entry['comment'];
|
||||
if (empty($entry['statement_entry_comment']) &&
|
||||
!empty($entry['comment']) &&
|
||||
$entry['type'] === 'CHARGE')
|
||||
$entry['statement_entry_comment'] = $entry['comment'];
|
||||
|
||||
// OK, add entry amount into the transaction total
|
||||
$transaction['amount'] += $entry['amount'];
|
||||
}
|
||||
|
||||
// Set the customer based on caller request, and set
|
||||
// ledger ID as the current ledger of the specified account
|
||||
$transaction['customer_id'] = $customer_id;
|
||||
$transaction['ledger_id'] =
|
||||
$this->Account->currentLedgerID($transaction['account_id']);
|
||||
|
||||
pr(array('Transaction::addTransaction' =>
|
||||
array('checkpoint' => 'Pre Transaction Save')
|
||||
+ compact('transaction')));
|
||||
|
||||
// Save transaction to the database
|
||||
$this->create();
|
||||
if (!$this->save($transaction))
|
||||
return array('error' => true);
|
||||
|
||||
// Set up our return ids array
|
||||
$ret = array('transaction_id' => $this->id,
|
||||
'Entry' => array(),
|
||||
'error' => false);
|
||||
|
||||
// Go through the entered charges
|
||||
foreach ($data['LedgerEntry'] AS $entry) {
|
||||
// Create the receipt entry, and reconcile the credit side
|
||||
// of the double-entry (which should be A/R) as a receipt.
|
||||
$ids = $this->LedgerEntry->Ledger->Account->postLedgerEntry
|
||||
($receipt,
|
||||
array('debit_ledger_id' => $A->currentLedgerID($entry['account_id']),
|
||||
'credit_ledger_id' => $A->currentLedgerID($A->receiptAccountID())
|
||||
) + $entry,
|
||||
array('debit' => 'receipt',
|
||||
'credit' => $reconcile)
|
||||
);
|
||||
|
||||
if ($ids['error'])
|
||||
$ret = false;
|
||||
foreach ($data['Entry'] AS $e_index => &$entry) {
|
||||
|
||||
$receipt = array_intersect_key($ids,
|
||||
array('receipt_id'=>1,
|
||||
'split_receipt_id'=>1));
|
||||
$entry['transaction_id'] = $this->id;
|
||||
$entry['customer_id'] = $customer_id;
|
||||
$entry['lease_id'] = $lease_id;
|
||||
$entry['ledger_id'] =
|
||||
$this->Account->currentLedgerID($entry['account_id']);
|
||||
|
||||
// Use ledger_entry_comment as the comment
|
||||
if (!empty($entry['ledger_entry_comment']))
|
||||
$entry['comment'] = $entry['ledger_entry_comment'];
|
||||
else
|
||||
$entry['comment'] = null;
|
||||
|
||||
pr(array('Transaction::addTransaction' =>
|
||||
array('checkpoint' => 'Pre Ledger Entry Save')
|
||||
+ array('entry' => array_diff_key($entry, array('Tender'=>1)))));
|
||||
|
||||
$LE = new LedgerEntry();
|
||||
$LE->create();
|
||||
if ($LE->save($entry))
|
||||
$ret['Entry'][$e_index]['ledger_entry_id'] = $LE->id;
|
||||
else {
|
||||
$ret['Entry'][$e_index]['ledger_entry_id'] = null;
|
||||
$ret['error'] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($entry['Tender'])) {
|
||||
// Come up with a non-unique name for the legal tender.
|
||||
// For checks & money orders, this will be based on the
|
||||
// check number. For cash, we'll just use the generic
|
||||
// name of the account.
|
||||
// REVISIT <AP>: 20090723
|
||||
// I would like to have cash named "Cash #1234", where
|
||||
// the number would correspond to either the Tender ID
|
||||
// or the LedgerEntry ID.
|
||||
if (empty($entry['Tender']['name'])) {
|
||||
$entry['Tender']['name'] = $this->Account->name($entry['account_id']);
|
||||
if ($entry['account_id'] == $this->Account->checkAccountID() ||
|
||||
$entry['account_id'] == $this->Account->moneyOrderAccountID()) {
|
||||
$entry['Tender']['name'] .= ' #' . $entry['Tender']['data1'];
|
||||
}
|
||||
}
|
||||
|
||||
$entry['Tender']['ledger_entry_id'] = $LE->id;
|
||||
|
||||
pr(array('Transaction::addTransaction' =>
|
||||
array('checkpoint' => 'Pre Tender Save')
|
||||
+ array('Tender' => $entry['Tender'])));
|
||||
|
||||
$Tender = new Tender();
|
||||
$Tender->create();
|
||||
if ($Tender->save($entry['Tender']))
|
||||
$ret['Entry'][$e_index]['tender_id'] = $Tender->id;
|
||||
else {
|
||||
$ret['Entry'][$e_index]['tender_id'] = null;
|
||||
$ret['error'] = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($entry['type'] === 'CHARGE') {
|
||||
// CHARGES need to be added as statement entries.
|
||||
// PAYMENTS will be added later after reconciliation
|
||||
|
||||
$SE = new StatementEntry();
|
||||
$SE->create();
|
||||
|
||||
// Use statement_entry_comment as the comment
|
||||
if (!empty($entry['statement_entry_comment']))
|
||||
$entry['comment'] = $entry['statement_entry_comment'];
|
||||
else
|
||||
$entry['comment'] = null;
|
||||
|
||||
pr(array('Transaction::addTransaction' =>
|
||||
array('checkpoint' => 'Pre Statement Entry Save')
|
||||
+ array('entry' => array_diff_key($entry, array('Tender'=>1)))));
|
||||
|
||||
if ($SE->save($entry))
|
||||
$ret['Entry'][$e_index]['statement_entry_id'] = $SE->id;
|
||||
else {
|
||||
$ret['Entry'][$e_index]['statement_entry_id'] = null;
|
||||
$ret['error'] = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// REVISIT <AP>: 20090723
|
||||
// Now that we have new entries, WE MUST RECONCILE
|
||||
// THE CHARGES TO CUSTOMER ACCOUNT BALANCE! This
|
||||
// is the only way "payments" are generated, and
|
||||
// the only way to make use of a positive customer
|
||||
// balance if new charges have been entered.
|
||||
|
||||
pr(array('Transaction::addTransaction' =>
|
||||
array('return' => $ret)));
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ function addChargeSource(flash) {
|
||||
($this->element('form_table',
|
||||
array('class' => "item invoice ledger-entry entry",
|
||||
//'with_name_after' => ':',
|
||||
'field_prefix' => 'LedgerEntry.%{id}',
|
||||
'field_prefix' => 'Entry.%{id}',
|
||||
'fields' => array
|
||||
("account_id" => array('name' => 'Account',
|
||||
'opts' =>
|
||||
@@ -164,11 +164,11 @@ function addChargeSource(flash) {
|
||||
),
|
||||
"effective_date" => array('opts' =>
|
||||
array('type' => 'text'),
|
||||
'between' => '<A HREF="#" ONCLICK="datepickerBOM(\'TransactionStamp\',\'LedgerEntry%{id}EffectiveDate\'); return false;">BOM</A>',
|
||||
'between' => '<A HREF="#" ONCLICK="datepickerBOM(\'TransactionStamp\',\'Entry%{id}EffectiveDate\'); return false;">BOM</A>',
|
||||
),
|
||||
"through_date" => array('opts' =>
|
||||
array('type' => 'text'),
|
||||
'between' => '<A HREF="#" ONCLICK="datepickerEOM(\'LedgerEntry%{id}EffectiveDate\',\'LedgerEntry%{id}ThroughDate\'); return false;">EOM</A>',
|
||||
'between' => '<A HREF="#" ONCLICK="datepickerEOM(\'Entry%{id}EffectiveDate\',\'Entry%{id}ThroughDate\'); return false;">EOM</A>',
|
||||
),
|
||||
"amount" => true,
|
||||
"comment" => array('opts' => array('size' => 50)),
|
||||
@@ -179,14 +179,14 @@ function addChargeSource(flash) {
|
||||
'</FIELDSET>'
|
||||
);
|
||||
|
||||
$("#LedgerEntry"+id+"EffectiveDate")
|
||||
$("#Entry"+id+"EffectiveDate")
|
||||
.attr('autocomplete', 'off')
|
||||
.datepicker({ constrainInput: true,
|
||||
numberOfMonths: [1, 1],
|
||||
showCurrentAtPos: 0,
|
||||
dateFormat: 'mm/dd/yy' });
|
||||
|
||||
$("#LedgerEntry"+id+"ThroughDate")
|
||||
$("#Entry"+id+"ThroughDate")
|
||||
.attr('autocomplete', 'off')
|
||||
.datepicker({ constrainInput: true,
|
||||
numberOfMonths: [1, 1],
|
||||
|
||||
Reference in New Issue
Block a user