fix: correct monthly actuals pre-population, void double-reversal, and cash flow history

- Monthly actuals grid now filters actual_amount to entry_type='monthly_actual'
  only, so other posted JEs in the same month don't bleed into the actuals UI
- Remove manual accounts.balance reversal from void() — the reversal JE's post()
  call already handles balance updates, preventing double-decrement per void
- Date void reversal entries to the original entry's date (not today) so
  historical monthly cash-flow periods stay accurate when actuals are re-edited
- Cash flow forecast now derives opening investment balances from investments
  purchased before the forecast start date rather than using current-snapshot
  totals, fixing historical months showing wrong investment balances

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 14:22:41 -04:00
parent 4df796e977
commit ba072b90f0
3 changed files with 26 additions and 17 deletions

View File

@@ -162,26 +162,18 @@ export class JournalEntriesService {
if (!je.is_posted) throw new BadRequestException('Cannot void an unposted entry'); if (!je.is_posted) throw new BadRequestException('Cannot void an unposted entry');
if (je.is_void) throw new BadRequestException('Already voided'); if (je.is_void) throw new BadRequestException('Already voided');
// Reverse account balances
for (const line of je.lines) {
const debit = parseFloat(line.debit) || 0;
const credit = parseFloat(line.credit) || 0;
const reverseAmount = credit - debit;
await this.tenant.query(
`UPDATE accounts SET balance = balance + $1, updated_at = NOW() WHERE id = $2`,
[reverseAmount, line.account_id],
);
}
await this.tenant.query( await this.tenant.query(
`UPDATE journal_entries SET is_void = true, voided_by = $1, voided_at = NOW(), void_reason = $2 WHERE id = $3`, `UPDATE journal_entries SET is_void = true, voided_by = $1, voided_at = NOW(), void_reason = $2 WHERE id = $3`,
[userId, reason, id], [userId, reason, id],
); );
// Create reversing entry // Create a reversing entry dated to the original entry's date so historical
// period balances stay accurate. The post() call handles accounts.balance updates —
// we do NOT manually reverse balances here to avoid double-counting.
const reverseDto: CreateJournalEntryDto = { const reverseDto: CreateJournalEntryDto = {
entryDate: new Date().toISOString().split('T')[0], entryDate: je.entry_date instanceof Date
? je.entry_date.toISOString().split('T')[0]
: String(je.entry_date).split('T')[0],
description: `VOID: ${je.description}`, description: `VOID: ${je.description}`,
referenceNumber: `VOID-${je.reference_number || je.id.slice(0, 8)}`, referenceNumber: `VOID-${je.reference_number || je.id.slice(0, 8)}`,
entryType: 'adjustment', entryType: 'adjustment',

View File

@@ -38,6 +38,7 @@ export class MonthlyActualsService {
LEFT JOIN journal_entry_lines jel ON jel.account_id = a.id LEFT JOIN journal_entry_lines jel ON jel.account_id = a.id
LEFT JOIN journal_entries je ON je.id = jel.journal_entry_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.is_posted = true AND je.is_void = false
AND je.entry_type = 'monthly_actual'
AND EXTRACT(YEAR FROM je.entry_date) = $1 AND EXTRACT(YEAR FROM je.entry_date) = $1
AND EXTRACT(MONTH FROM je.entry_date) = $2 AND EXTRACT(MONTH FROM je.entry_date) = $2
WHERE a.is_active = true WHERE a.is_active = true

View File

@@ -1089,9 +1089,25 @@ export class ReportsService {
else projectIndex[key].reserve += cost; else projectIndex[key].reserve += cost;
} }
// Investment opening balances at start of period (approximate: use current values) // Investment balances at the start of the period — computed from the investment_accounts
let runOpInv = opInv; // table as of startYear-01-01. We use current_value for all active investments that
let runResInv = resInv; // existed before startYear (purchase_date < startYear-01-01). Investments purchased
// after that date will be added when their purchase month is processed in the forecast loop.
const openingInvOp = await this.tenant.query(`
SELECT COALESCE(SUM(current_value), 0) as total
FROM investment_accounts
WHERE fund_type = 'operating' AND is_active = true
AND (purchase_date IS NULL OR purchase_date < $1::date)
`, [`${startYear}-01-01`]);
const openingInvRes = await this.tenant.query(`
SELECT COALESCE(SUM(current_value), 0) as total
FROM investment_accounts
WHERE fund_type = 'reserve' AND is_active = true
AND (purchase_date IS NULL OR purchase_date < $1::date)
`, [`${startYear}-01-01`]);
let runOpInv = parseFloat(openingInvOp[0]?.total || '0');
let runResInv = parseFloat(openingInvRes[0]?.total || '0');
// Determine which months have actual journal entries // Determine which months have actual journal entries
// A month is "actual" only if it's not in the future AND has real journal entry data // A month is "actual" only if it's not in the future AND has real journal entry data