Accounting audit fixes: revenue default IsActive + deposit account guard
Audit of this session's accounting changes (sub-type→type dropdowns, deposit account picker, default GL accounts) found no ledger-drift bugs. Two fixes applied: - Default revenue account now requires IsActive (mirrors the 4000 fallback), so a deactivated default isn't silently posted to. - DepositsController.Record blocks recording when the 2300 Customer Deposits liability exists but no deposit/bank account resolves — that would post a one-sided entry. When 2300 doesn't exist (no accounting), nothing posts, so the deposit is still allowed. ACCOUNTING_AUDIT.md updated: O9 footgun surface widened by the default- accounts feature (now mitigated/documented), plus the 2026-06-20 review notes and the resolved deposit-imbalance item. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -97,6 +97,20 @@ public class DepositsController : Controller
|
||||
}
|
||||
depositAcctId ??= await GetCheckingAccountIdAsync(currentUser.CompanyId);
|
||||
|
||||
// Guard against an unbalanced GL posting: this deposit credits the Customer Deposits
|
||||
// liability (2300). If that account exists but we have no bank/asset account to debit,
|
||||
// the entry would be one-sided. Block it so the user picks a deposit account first.
|
||||
// (When 2300 doesn't exist — e.g. a company not using accounting — no GL posts at all,
|
||||
// so a missing bank account is harmless and the deposit is allowed through.)
|
||||
var custDepositsAcctId = await GetCustomerDepositsAccountIdAsync(currentUser.CompanyId);
|
||||
if (custDepositsAcctId != null && depositAcctId == null)
|
||||
return Json(new
|
||||
{
|
||||
success = false,
|
||||
message = "Select a deposit account (the bank/asset account this payment lands in) " +
|
||||
"before recording. None is configured for your company yet."
|
||||
});
|
||||
|
||||
var deposit = new Deposit
|
||||
{
|
||||
ReceiptNumber = receiptNumber,
|
||||
@@ -119,7 +133,6 @@ public class DepositsController : Controller
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
// GL: DR Checking (cash received) / CR Customer Deposits 2300 (liability until applied to invoice).
|
||||
var custDepositsAcctId = await GetCustomerDepositsAccountIdAsync(currentUser.CompanyId);
|
||||
await _accountBalanceService.DebitAsync(depositAcctId, deposit.Amount);
|
||||
await _accountBalanceService.CreditAsync(custDepositsAcctId, deposit.Amount);
|
||||
|
||||
|
||||
@@ -413,10 +413,13 @@ public class InvoicesController : Controller
|
||||
: new Dictionary<int, CatalogItem>();
|
||||
|
||||
// Fall back to the company's configured default revenue account when a catalog item
|
||||
// has no specific account; if none is configured, fall back to the seeded 4000 account.
|
||||
// has no specific account; if none is configured (or it has since been deactivated),
|
||||
// fall back to the seeded 4000 account. The IsActive check mirrors the 4000 lookup so a
|
||||
// deactivated default doesn't keep being posted to.
|
||||
Account? defaultRevenueAccount = null;
|
||||
if (prefs?.DefaultRevenueAccountId != null)
|
||||
defaultRevenueAccount = await _unitOfWork.Accounts.GetByIdAsync(prefs.DefaultRevenueAccountId.Value);
|
||||
defaultRevenueAccount = await _unitOfWork.Accounts.FirstOrDefaultAsync(
|
||||
a => a.Id == prefs.DefaultRevenueAccountId.Value && a.IsActive);
|
||||
defaultRevenueAccount ??= await _unitOfWork.Accounts
|
||||
.FirstOrDefaultAsync(a => a.AccountNumber == "4000" && a.IsActive);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user