fix: resolve critical SQL and display bugs across 5 financial reports
- Fix systemic LEFT JOIN date filter bug in Balance Sheet, Income Statement, and Cash Flow Statement by using parenthesized INNER JOIN pattern so SUM(jel.debit/credit) respects date parameters - Add Current Year Net Income synthetic equity line to Balance Sheet to satisfy the accounting equation (A = L + E) during open fiscal periods - Add investment_accounts balances to Balance Sheet assets and corresponding equity lines for reserve/operating investment holdings - Fix Cash Flow Statement beginning/ending cash always showing $0 by replacing LIKE '%Cash%' filter with account_type = 'asset' - Fix Year-End Package HTTP 500 by replacing broken invoices.vendor_id query with journal-entry-based vendor payment lookup - Fix Quarterly Report defaulting to previous quarter instead of current - Fix Quarterly Report date subtitle off-by-one day from UTC parsing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,10 +14,12 @@ export class ReportsService {
|
||||
ELSE COALESCE(SUM(jel.credit), 0) - COALESCE(SUM(jel.debit), 0)
|
||||
END as balance
|
||||
FROM accounts a
|
||||
LEFT 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
|
||||
AND je.entry_date <= $1
|
||||
LEFT JOIN (
|
||||
journal_entry_lines jel
|
||||
INNER 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
|
||||
) ON jel.account_id = a.id
|
||||
WHERE a.is_active = true AND a.account_type IN ('asset', 'liability', 'equity')
|
||||
GROUP BY a.id, a.account_number, a.name, a.account_type, a.fund_type
|
||||
HAVING CASE
|
||||
@@ -32,6 +34,71 @@ export class ReportsService {
|
||||
const liabilities = rows.filter((r: any) => r.account_type === 'liability');
|
||||
const equity = rows.filter((r: any) => r.account_type === 'equity');
|
||||
|
||||
// Compute current year net income (income - expenses) for the fiscal year through as_of date
|
||||
// This balances the accounting equation: Assets = Liabilities + Equity + Net Income
|
||||
const fiscalYearStart = `${asOf.substring(0, 4)}-01-01`;
|
||||
const netIncomeSql = `
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN a.account_type = 'income'
|
||||
THEN jel.credit - jel.debit ELSE 0 END), 0) -
|
||||
COALESCE(SUM(CASE WHEN a.account_type = 'expense'
|
||||
THEN jel.debit - jel.credit ELSE 0 END), 0) as net_income
|
||||
FROM journal_entry_lines jel
|
||||
INNER 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 BETWEEN $1 AND $2
|
||||
INNER JOIN accounts a ON a.id = jel.account_id
|
||||
AND a.account_type IN ('income', 'expense') AND a.is_active = true
|
||||
`;
|
||||
const netIncomeResult = await this.tenant.query(netIncomeSql, [fiscalYearStart, asOf]);
|
||||
const netIncome = parseFloat(netIncomeResult[0]?.net_income || '0');
|
||||
|
||||
// Add current year net income as a synthetic equity line
|
||||
if (netIncome !== 0) {
|
||||
equity.push({
|
||||
id: null,
|
||||
account_number: '',
|
||||
name: 'Current Year Net Income',
|
||||
account_type: 'equity',
|
||||
fund_type: 'operating',
|
||||
balance: netIncome.toFixed(2),
|
||||
});
|
||||
}
|
||||
|
||||
// Add investment account balances to assets and corresponding equity
|
||||
const investmentsSql = `
|
||||
SELECT id, name, institution, current_value as balance, fund_type
|
||||
FROM investment_accounts
|
||||
WHERE is_active = true AND current_value > 0
|
||||
`;
|
||||
const investments = await this.tenant.query(investmentsSql);
|
||||
const investmentsByFund: Record<string, number> = {};
|
||||
for (const inv of investments) {
|
||||
assets.push({
|
||||
id: inv.id,
|
||||
account_number: '',
|
||||
name: `${inv.name} (${inv.institution})`,
|
||||
account_type: 'asset',
|
||||
fund_type: inv.fund_type,
|
||||
balance: parseFloat(inv.balance).toFixed(2),
|
||||
});
|
||||
investmentsByFund[inv.fund_type] = (investmentsByFund[inv.fund_type] || 0) + parseFloat(inv.balance);
|
||||
}
|
||||
// Add investment balances as synthetic equity lines to maintain A = L + E
|
||||
for (const [fundType, total] of Object.entries(investmentsByFund)) {
|
||||
if (total > 0) {
|
||||
const label = fundType === 'reserve' ? 'Reserve' : 'Operating';
|
||||
equity.push({
|
||||
id: null,
|
||||
account_number: '',
|
||||
name: `${label} Investment Holdings`,
|
||||
account_type: 'equity',
|
||||
fund_type: fundType,
|
||||
balance: total.toFixed(2),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const totalAssets = assets.reduce((s: number, r: any) => s + parseFloat(r.balance), 0);
|
||||
const totalLiabilities = liabilities.reduce((s: number, r: any) => s + parseFloat(r.balance), 0);
|
||||
const totalEquity = equity.reduce((s: number, r: any) => s + parseFloat(r.balance), 0);
|
||||
@@ -54,10 +121,12 @@ export class ReportsService {
|
||||
ELSE COALESCE(SUM(jel.debit), 0) - COALESCE(SUM(jel.credit), 0)
|
||||
END as amount
|
||||
FROM accounts a
|
||||
LEFT 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
|
||||
AND je.entry_date BETWEEN $1 AND $2
|
||||
LEFT JOIN (
|
||||
journal_entry_lines jel
|
||||
INNER 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 BETWEEN $1 AND $2
|
||||
) ON jel.account_id = a.id
|
||||
WHERE a.is_active = true AND a.account_type IN ('income', 'expense')
|
||||
GROUP BY a.id, a.account_number, a.name, a.account_type, a.fund_type
|
||||
HAVING CASE
|
||||
@@ -340,20 +409,20 @@ export class ReportsService {
|
||||
ORDER BY a.name
|
||||
`, [from, to]);
|
||||
|
||||
// Asset filter: cash-only vs cash + investment accounts
|
||||
const assetFilter = includeInvestments
|
||||
? `a.account_type = 'asset'`
|
||||
: `a.account_type = 'asset' AND a.name LIKE '%Cash%'`;
|
||||
// Asset filter: all asset accounts (bank/checking/savings are the cash accounts)
|
||||
const assetFilter = `a.account_type = 'asset'`;
|
||||
|
||||
// Cash beginning and ending balances
|
||||
const beginCash = await this.tenant.query(`
|
||||
SELECT COALESCE(SUM(sub.bal), 0) as balance FROM (
|
||||
SELECT COALESCE(SUM(jel.debit), 0) - COALESCE(SUM(jel.credit), 0) as bal
|
||||
FROM accounts a
|
||||
LEFT 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
|
||||
AND je.entry_date < $1
|
||||
LEFT JOIN (
|
||||
journal_entry_lines jel
|
||||
INNER 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
|
||||
) ON jel.account_id = a.id
|
||||
WHERE ${assetFilter} AND a.is_active = true
|
||||
GROUP BY a.id
|
||||
) sub
|
||||
@@ -363,10 +432,12 @@ export class ReportsService {
|
||||
SELECT COALESCE(SUM(sub.bal), 0) as balance FROM (
|
||||
SELECT COALESCE(SUM(jel.debit), 0) - COALESCE(SUM(jel.credit), 0) as bal
|
||||
FROM accounts a
|
||||
LEFT 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
|
||||
AND je.entry_date <= $1
|
||||
LEFT JOIN (
|
||||
journal_entry_lines jel
|
||||
INNER 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
|
||||
) ON jel.account_id = a.id
|
||||
WHERE ${assetFilter} AND a.is_active = true
|
||||
GROUP BY a.id
|
||||
) sub
|
||||
@@ -479,19 +550,22 @@ export class ReportsService {
|
||||
const incomeStmt = await this.getIncomeStatement(from, to);
|
||||
const balanceSheet = await this.getBalanceSheet(to);
|
||||
|
||||
// 1099 vendor data
|
||||
// 1099 vendor data — uses journal entries via vendor's default_account_id
|
||||
const vendors1099 = await this.tenant.query(`
|
||||
SELECT v.id, v.name, v.tax_id, v.address_line1, v.city, v.state, v.zip_code,
|
||||
COALESCE(SUM(p.amount), 0) as total_paid
|
||||
COALESCE(SUM(p_amounts.amount), 0) as total_paid
|
||||
FROM vendors v
|
||||
JOIN (
|
||||
SELECT vendor_id, amount FROM invoices
|
||||
WHERE EXTRACT(YEAR FROM invoice_date) = $1
|
||||
AND status IN ('paid', 'partial')
|
||||
) p ON p.vendor_id = v.id
|
||||
LEFT JOIN (
|
||||
SELECT jel.account_id, jel.debit as amount
|
||||
FROM journal_entry_lines jel
|
||||
JOIN journal_entries je ON je.id = jel.journal_entry_id
|
||||
WHERE je.is_posted = true AND je.is_void = false
|
||||
AND EXTRACT(YEAR FROM je.entry_date) = $1
|
||||
AND jel.debit > 0
|
||||
) p_amounts ON p_amounts.account_id = v.default_account_id
|
||||
WHERE v.is_1099_eligible = true
|
||||
GROUP BY v.id, v.name, v.tax_id, v.address_line1, v.city, v.state, v.zip_code
|
||||
HAVING COALESCE(SUM(p.amount), 0) >= 600
|
||||
HAVING COALESCE(SUM(p_amounts.amount), 0) >= 600
|
||||
ORDER BY v.name
|
||||
`, [year]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user