Recompute inventory-consumption COGS and fix written-off AR (audit O6, O8)
O6: inventory consumed on jobs posts DR COGS / CR Inventory, but neither recompute
engine reflected it — so reports understated COGS / overstated inventory and a
"Recalculate Balances" wiped the effect. The COGS posting fires only for JobUsage
and Waste transaction types, which are created only at the two COGS-posting sites,
so the consumption is exactly identifiable from InventoryTransaction:
- both posting sites now record consumption at the effective (weighted-average)
unit cost so TotalCost equals the COGS posted (the recompute reads TotalCost)
- LedgerService: new section (dated rows + prior balance) crediting Inventory /
debiting COGS from JobUsage/Waste rows on items with both accounts mapped
- FinancialReportService: Trial Balance + accrual P&L include consumption COGS
This reads existing transactions, so historical data is covered with no backfill.
The Balance Sheet inventory line is intentionally left alone — it does not track
inventory purchases either (periodic), so relieving it for consumption alone would
unbalance it; tracked as O9 (inventory capitalization policy).
O8: the write-off already creates a balanced posted JournalEntry (both engines read
it via their JE-line sections). The real defect was 4 "Status != WrittenOff" filters
in FinancialReportService that excluded pre-write-off payments from AR credits and
bank debits — leaving the paid portion dangling as open AR and understating the bank.
Removed those filters; AR now nets to zero for written-off invoices and the trial
balance balances. No backfill needed.
Adds a LedgerService regression test for inventory consumption. Build clean; 293
unit tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -600,6 +600,41 @@ public class LedgerService : ILedgerService
|
||||
});
|
||||
}
|
||||
|
||||
// ── 12d. Inventory consumption COGS (DR COGS / CR Inventory) ──────────
|
||||
// When an item with both a COGS and an Inventory account is consumed (JobUsage/Waste — the only
|
||||
// two transaction types created at the COGS-posting sites), JobsController/InventoryController post
|
||||
// DR COGS / CR Inventory at the transaction's TotalCost. Reproduce it here so a balance recompute
|
||||
// matches the posting and the trial balance stays balanced. TotalCost is stored positive.
|
||||
if (account.AccountType == AccountType.CostOfGoods || account.AccountSubType == AccountSubType.Inventory)
|
||||
{
|
||||
var consumption = await _context.InventoryTransactions
|
||||
.Include(t => t.InventoryItem)
|
||||
.Where(t => (t.TransactionType == InventoryTransactionType.JobUsage || t.TransactionType == InventoryTransactionType.Waste)
|
||||
&& t.InventoryItem.CogsAccountId != null && t.InventoryItem.InventoryAccountId != null
|
||||
&& (t.InventoryItem.CogsAccountId == accountId || t.InventoryItem.InventoryAccountId == accountId)
|
||||
&& t.TransactionDate >= fromDate && t.TransactionDate <= toDate)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var t in consumption)
|
||||
{
|
||||
var amount = Math.Abs(t.TotalCost);
|
||||
if (t.InventoryItem.CogsAccountId == accountId)
|
||||
entries.Add(new LedgerEntryDto
|
||||
{
|
||||
Date = t.TransactionDate, Reference = t.Reference ?? $"INV-{t.Id}",
|
||||
Source = "Inventory Usage", Description = $"COGS — {t.InventoryItem.Name}",
|
||||
Debit = amount, Credit = 0, LinkController = "Inventory", LinkId = t.InventoryItemId
|
||||
});
|
||||
if (t.InventoryItem.InventoryAccountId == accountId)
|
||||
entries.Add(new LedgerEntryDto
|
||||
{
|
||||
Date = t.TransactionDate, Reference = t.Reference ?? $"INV-{t.Id}",
|
||||
Source = "Inventory Usage", Description = $"Inventory relieved — {t.InventoryItem.Name}",
|
||||
Debit = 0, Credit = amount, LinkController = "Inventory", LinkId = t.InventoryItemId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── 10. Journal Entry lines touching this account ──────────────────
|
||||
var jeLines = await _context.JournalEntryLines
|
||||
.Include(l => l.JournalEntry)
|
||||
@@ -855,6 +890,26 @@ public class LedgerService : ILedgerService
|
||||
.SumAsync(m => (decimal?)(m.Amount - m.AmountApplied)) ?? 0;
|
||||
}
|
||||
|
||||
// 12d. Inventory consumption COGS (DR COGS / CR Inventory). Mirrors section 12d in
|
||||
// GetAccountLedgerAsync so the prior-period opening balance matches the posting.
|
||||
if (account.AccountType == AccountType.CostOfGoods || account.AccountSubType == AccountSubType.Inventory)
|
||||
{
|
||||
var priorConsumption = await _context.InventoryTransactions
|
||||
.Include(t => t.InventoryItem)
|
||||
.Where(t => (t.TransactionType == InventoryTransactionType.JobUsage || t.TransactionType == InventoryTransactionType.Waste)
|
||||
&& t.InventoryItem.CogsAccountId != null && t.InventoryItem.InventoryAccountId != null
|
||||
&& (t.InventoryItem.CogsAccountId == accountId || t.InventoryItem.InventoryAccountId == accountId)
|
||||
&& t.TransactionDate < beforeDate)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var t in priorConsumption)
|
||||
{
|
||||
var amount = Math.Abs(t.TotalCost);
|
||||
if (t.InventoryItem.CogsAccountId == accountId) debits += amount;
|
||||
if (t.InventoryItem.InventoryAccountId == accountId) credits += amount;
|
||||
}
|
||||
}
|
||||
|
||||
// 10. Posted journal entry lines touching this account (prior to period)
|
||||
debits += await _context.JournalEntryLines
|
||||
.Where(l => l.AccountId == accountId
|
||||
|
||||
Reference in New Issue
Block a user