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:
@@ -162,26 +162,18 @@ export class JournalEntriesService {
|
||||
if (!je.is_posted) throw new BadRequestException('Cannot void an unposted entry');
|
||||
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(
|
||||
`UPDATE journal_entries SET is_void = true, voided_by = $1, voided_at = NOW(), void_reason = $2 WHERE id = $3`,
|
||||
[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 = {
|
||||
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}`,
|
||||
referenceNumber: `VOID-${je.reference_number || je.id.slice(0, 8)}`,
|
||||
entryType: 'adjustment',
|
||||
|
||||
@@ -38,6 +38,7 @@ export class MonthlyActualsService {
|
||||
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_type = 'monthly_actual'
|
||||
AND EXTRACT(YEAR FROM je.entry_date) = $1
|
||||
AND EXTRACT(MONTH FROM je.entry_date) = $2
|
||||
WHERE a.is_active = true
|
||||
|
||||
@@ -1089,9 +1089,25 @@ export class ReportsService {
|
||||
else projectIndex[key].reserve += cost;
|
||||
}
|
||||
|
||||
// Investment opening balances at start of period (approximate: use current values)
|
||||
let runOpInv = opInv;
|
||||
let runResInv = resInv;
|
||||
// Investment balances at the start of the period — computed from the investment_accounts
|
||||
// table as of startYear-01-01. We use current_value for all active investments that
|
||||
// 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
|
||||
// A month is "actual" only if it's not in the future AND has real journal entry data
|
||||
|
||||
Reference in New Issue
Block a user