Fix cash flow forecast double-counting by using asset accounts consistently
The forecast was counting money twice: operating used asset accounts while reserve used equity accounts, but both sides of the opening balance journal entry represent the same funds. Changed all cash balance queries (current, opening, and historical) to use asset accounts (debit - credit) for both operating and reserve. Also fixed a LEFT JOIN bug where date filters on journal_entries didn't prevent journal_entry_lines from being summed, causing opening balances to include all entries regardless of date. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -508,25 +508,25 @@ export class ReportsService {
|
|||||||
const monthLabels = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
const monthLabels = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
||||||
|
|
||||||
// ── 1) Get current balances as of now ──
|
// ── 1) Get current balances as of now ──
|
||||||
// Operating cash (asset accounts with fund_type=operating)
|
// Use ASSET accounts for both operating and reserve — these represent actual cash holdings.
|
||||||
|
// Equity accounts are the bookkeeping counterpart and would double-count.
|
||||||
const opCashRows = await this.tenant.query(`
|
const opCashRows = await this.tenant.query(`
|
||||||
SELECT COALESCE(SUM(sub.bal), 0) as total FROM (
|
SELECT COALESCE(SUM(sub.bal), 0) as total FROM (
|
||||||
SELECT COALESCE(SUM(jel.debit), 0) - COALESCE(SUM(jel.credit), 0) as bal
|
SELECT COALESCE(SUM(jel.debit), 0) - COALESCE(SUM(jel.credit), 0) as bal
|
||||||
FROM accounts a
|
FROM accounts a
|
||||||
LEFT JOIN journal_entry_lines jel ON jel.account_id = a.id
|
JOIN journal_entry_lines jel ON jel.account_id = a.id
|
||||||
LEFT JOIN journal_entries je ON je.id = jel.journal_entry_id AND je.is_posted = true AND je.is_void = false
|
JOIN journal_entries je ON je.id = jel.journal_entry_id AND je.is_posted = true AND je.is_void = false
|
||||||
WHERE a.account_type = 'asset' AND a.fund_type = 'operating' AND a.is_active = true
|
WHERE a.account_type = 'asset' AND a.fund_type = 'operating' AND a.is_active = true
|
||||||
GROUP BY a.id
|
GROUP BY a.id
|
||||||
) sub
|
) sub
|
||||||
`);
|
`);
|
||||||
// Reserve cash (equity fund balance for reserve)
|
|
||||||
const resCashRows = await this.tenant.query(`
|
const resCashRows = await this.tenant.query(`
|
||||||
SELECT COALESCE(SUM(sub.bal), 0) as total FROM (
|
SELECT COALESCE(SUM(sub.bal), 0) as total FROM (
|
||||||
SELECT COALESCE(SUM(jel.credit), 0) - COALESCE(SUM(jel.debit), 0) as bal
|
SELECT COALESCE(SUM(jel.debit), 0) - COALESCE(SUM(jel.credit), 0) as bal
|
||||||
FROM accounts a
|
FROM accounts a
|
||||||
LEFT JOIN journal_entry_lines jel ON jel.account_id = a.id
|
JOIN journal_entry_lines jel ON jel.account_id = a.id
|
||||||
LEFT JOIN journal_entries je ON je.id = jel.journal_entry_id AND je.is_posted = true AND je.is_void = false
|
JOIN journal_entries je ON je.id = jel.journal_entry_id AND je.is_posted = true AND je.is_void = false
|
||||||
WHERE a.fund_type = 'reserve' AND a.account_type = 'equity' AND a.is_active = true
|
WHERE a.account_type = 'asset' AND a.fund_type = 'reserve' AND a.is_active = true
|
||||||
GROUP BY a.id
|
GROUP BY a.id
|
||||||
) sub
|
) sub
|
||||||
`);
|
`);
|
||||||
@@ -641,13 +641,14 @@ export class ReportsService {
|
|||||||
|
|
||||||
// For historical months, compute cumulative balances from journal entries
|
// For historical months, compute cumulative balances from journal entries
|
||||||
// We'll track running balances
|
// We'll track running balances
|
||||||
// First compute opening balance at start of startYear
|
// First compute opening balance at start of startYear using asset accounts
|
||||||
const openingOp = await this.tenant.query(`
|
const openingOp = await this.tenant.query(`
|
||||||
SELECT COALESCE(SUM(sub.bal), 0) as total FROM (
|
SELECT COALESCE(SUM(sub.bal), 0) as total FROM (
|
||||||
SELECT COALESCE(SUM(jel.debit), 0) - COALESCE(SUM(jel.credit), 0) as bal
|
SELECT COALESCE(SUM(jel.debit), 0) - COALESCE(SUM(jel.credit), 0) as bal
|
||||||
FROM accounts a
|
FROM accounts a
|
||||||
LEFT JOIN journal_entry_lines jel ON jel.account_id = a.id
|
JOIN journal_entry_lines jel ON jel.account_id = a.id
|
||||||
LEFT JOIN journal_entries je ON je.id = jel.journal_entry_id AND je.is_posted = true AND je.is_void = false
|
JOIN journal_entries je ON je.id = jel.journal_entry_id
|
||||||
|
AND je.is_posted = true AND je.is_void = false
|
||||||
AND je.entry_date < $1::date
|
AND je.entry_date < $1::date
|
||||||
WHERE a.account_type = 'asset' AND a.fund_type = 'operating' AND a.is_active = true
|
WHERE a.account_type = 'asset' AND a.fund_type = 'operating' AND a.is_active = true
|
||||||
GROUP BY a.id
|
GROUP BY a.id
|
||||||
@@ -656,12 +657,13 @@ export class ReportsService {
|
|||||||
|
|
||||||
const openingRes = await this.tenant.query(`
|
const openingRes = await this.tenant.query(`
|
||||||
SELECT COALESCE(SUM(sub.bal), 0) as total FROM (
|
SELECT COALESCE(SUM(sub.bal), 0) as total FROM (
|
||||||
SELECT COALESCE(SUM(jel.credit), 0) - COALESCE(SUM(jel.debit), 0) as bal
|
SELECT COALESCE(SUM(jel.debit), 0) - COALESCE(SUM(jel.credit), 0) as bal
|
||||||
FROM accounts a
|
FROM accounts a
|
||||||
LEFT JOIN journal_entry_lines jel ON jel.account_id = a.id
|
JOIN journal_entry_lines jel ON jel.account_id = a.id
|
||||||
LEFT JOIN journal_entries je ON je.id = jel.journal_entry_id AND je.is_posted = true AND je.is_void = false
|
JOIN journal_entries je ON je.id = jel.journal_entry_id
|
||||||
|
AND je.is_posted = true AND je.is_void = false
|
||||||
AND je.entry_date < $1::date
|
AND je.entry_date < $1::date
|
||||||
WHERE a.fund_type = 'reserve' AND a.account_type = 'equity' AND a.is_active = true
|
WHERE a.account_type = 'asset' AND a.fund_type = 'reserve' AND a.is_active = true
|
||||||
GROUP BY a.id
|
GROUP BY a.id
|
||||||
) sub
|
) sub
|
||||||
`, [`${startYear}-01-01`]);
|
`, [`${startYear}-01-01`]);
|
||||||
@@ -719,19 +721,13 @@ export class ReportsService {
|
|||||||
const label = `${monthLabels[month - 1]} ${year}`;
|
const label = `${monthLabels[month - 1]} ${year}`;
|
||||||
|
|
||||||
if (isHistorical) {
|
if (isHistorical) {
|
||||||
// Use actual journal entry changes
|
// Use actual journal entry changes from asset accounts
|
||||||
const opChange = histIndex[`${year}-${month}-operating`] || 0;
|
const opChange = histIndex[`${year}-${month}-operating`] || 0;
|
||||||
runOpCash += opChange;
|
runOpCash += opChange;
|
||||||
|
|
||||||
// For reserve, we need the equity-based changes
|
// Reserve also uses asset account changes (already in histIndex from section 4)
|
||||||
const resEquityChange = await this.tenant.query(`
|
const resChange = histIndex[`${year}-${month}-reserve`] || 0;
|
||||||
SELECT COALESCE(SUM(jel.credit - jel.debit), 0) as net
|
runResCash += resChange;
|
||||||
FROM journal_entry_lines jel
|
|
||||||
JOIN journal_entries je ON je.id = jel.journal_entry_id AND je.is_posted = true AND je.is_void = false
|
|
||||||
JOIN accounts a ON a.id = jel.account_id AND a.fund_type = 'reserve' AND a.account_type = 'equity'
|
|
||||||
WHERE EXTRACT(YEAR FROM je.entry_date) = $1 AND EXTRACT(MONTH FROM je.entry_date) = $2
|
|
||||||
`, [year, month]);
|
|
||||||
runResCash += parseFloat(resEquityChange[0]?.net || '0');
|
|
||||||
|
|
||||||
datapoints.push({
|
datapoints.push({
|
||||||
month: label,
|
month: label,
|
||||||
|
|||||||
Reference in New Issue
Block a user