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_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',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user