Enhance financial report accuracy by including closing journals in profit and loss calculations and trial balances; add idempotency check for journal entry creation.
This commit is contained in:
@@ -88,7 +88,8 @@ namespace General_Ledger_Service.Repositories
|
||||
// its reversal) showed equal/duplicated figures in both columns.
|
||||
public async Task<List<TrialBalanceRowDto>> GetTrialBalanceAsync(DateOnly asOfDate)
|
||||
{
|
||||
var totals = await GetPostedTotalsAsync(null, asOfDate);
|
||||
// Post-closing trial balance: includes year-end closing journals (JournalType CLOSING).
|
||||
var totals = await GetPostedTotalsAsync(null, asOfDate, includeClosing: true);
|
||||
var postableAccounts = await _context.Accounts.AsNoTracking().Where(a => a.IsPostable).ToListAsync();
|
||||
|
||||
return [.. postableAccounts
|
||||
@@ -120,7 +121,8 @@ namespace General_Ledger_Service.Repositories
|
||||
var accounts = await _context.Accounts.AsNoTracking().ToListAsync();
|
||||
var roots = accounts.Where(a => a.ParentAccountId == null && (a.AccountTypeId == 1 || a.AccountTypeId == 2 || a.AccountTypeId == 3));
|
||||
var classification = InheritTag(accounts, roots, a => a.BalanceSheetClassification);
|
||||
var totals = await GetPostedTotalsAsync(null, asOfDate);
|
||||
// Includes closing journals so Retained Earnings carries every closed year's profit.
|
||||
var totals = await GetPostedTotalsAsync(null, asOfDate, includeClosing: true);
|
||||
|
||||
return [.. accounts
|
||||
.Where(a => a.IsPostable && classification.ContainsKey(a.AccountId) && totals.ContainsKey(a.AccountId))
|
||||
@@ -141,12 +143,12 @@ namespace General_Ledger_Service.Repositories
|
||||
// pattern as cash_flow_category); ReportService groups these into the nested
|
||||
// ProfitAndLossReportDto and applies the Expense-side sign flip (Amount here is the raw,
|
||||
// unflipped credit - debit - negative for a normal debit-balance Expense account).
|
||||
public async Task<List<ProfitAndLossLeafRowDto>> GetProfitAndLossLeafRowsAsync(DateOnly periodStart, DateOnly periodEnd)
|
||||
public async Task<List<ProfitAndLossLeafRowDto>> GetProfitAndLossLeafRowsAsync(DateOnly periodStart, DateOnly periodEnd, bool includeClosing = false)
|
||||
{
|
||||
var accounts = await _context.Accounts.AsNoTracking().ToListAsync();
|
||||
var roots = accounts.Where(a => a.ParentAccountId == null && (a.AccountTypeId == 4 || a.AccountTypeId == 5));
|
||||
var section = InheritTag(accounts, roots, a => a.IncomeStatementSection);
|
||||
var totals = await GetPostedTotalsAsync(periodStart, periodEnd);
|
||||
var totals = await GetPostedTotalsAsync(periodStart, periodEnd, includeClosing);
|
||||
|
||||
return [.. accounts
|
||||
.Where(a => a.IsPostable && section.ContainsKey(a.AccountId) && totals.ContainsKey(a.AccountId))
|
||||
@@ -254,10 +256,21 @@ namespace General_Ledger_Service.Repositories
|
||||
// Per-account base debit/credit totals over posted journal entries, optionally bounded
|
||||
// by entry date (inclusive on both ends). The sums run in the database; accounts with no
|
||||
// matching lines are simply absent from the result.
|
||||
private async Task<Dictionary<long, (decimal Debit, decimal Credit)>> GetPostedTotalsAsync(DateOnly? from, DateOnly? to)
|
||||
// Year-end closing journals (JournalType == ClosingJournalType) move a year's income/expense into
|
||||
// Retained Earnings. They are excluded unless includeClosing is true, so the P&L, cash flow and
|
||||
// similar period reports still show a closed year's own activity; the balance sheet and trial
|
||||
// balance include them so every account's balance is complete.
|
||||
public const string ClosingJournalType = "CLOSING";
|
||||
|
||||
private async Task<Dictionary<long, (decimal Debit, decimal Credit)>> GetPostedTotalsAsync(DateOnly? from, DateOnly? to, bool includeClosing = false)
|
||||
{
|
||||
var lines = _context.JournalEntryLines.Where(l => l.Journal.IsPosted);
|
||||
|
||||
if (!includeClosing)
|
||||
{
|
||||
lines = lines.Where(l => l.Journal.JournalType != ClosingJournalType);
|
||||
}
|
||||
|
||||
if (from.HasValue)
|
||||
{
|
||||
lines = lines.Where(l => l.Journal.EntryDate >= from.Value);
|
||||
|
||||
@@ -44,6 +44,7 @@ namespace General_Ledger_Service.Repositories
|
||||
// Actual = posted base debit - credit per (account, period), summed in the database.
|
||||
var actuals = await _context.JournalEntryLines
|
||||
.Where(l => l.Journal.IsPosted
|
||||
&& l.Journal.JournalType != "CLOSING"
|
||||
&& accountIds.Contains(l.AccountId)
|
||||
&& periodIds.Contains(l.Journal.PeriodId))
|
||||
.GroupBy(l => new { l.AccountId, l.Journal.PeriodId })
|
||||
|
||||
@@ -15,6 +15,11 @@ namespace General_Ledger_Service.Repositories
|
||||
.Include(j => j.Lines)
|
||||
.FirstOrDefaultAsync(j => j.JournalNo == journalNo);
|
||||
|
||||
public async Task<JournalEntry?> GetJournalEntryBySourceReferenceAsync(string sourceModule, string reference) =>
|
||||
await _context.JournalEntries
|
||||
.Include(j => j.Lines)
|
||||
.FirstOrDefaultAsync(j => j.SourceModule == sourceModule && j.Reference == reference);
|
||||
|
||||
public async Task<int> CountJournalEntriesForYearAsync(int year) =>
|
||||
await _context.JournalEntries.CountAsync(j => j.JournalNo.StartsWith($"JV-{year}-"));
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ namespace General_Ledger_Service.Repositories
|
||||
{
|
||||
var net = await _context.JournalEntryLines
|
||||
.Where(l => l.Journal.IsPosted
|
||||
&& l.Journal.JournalType != "CLOSING"
|
||||
&& l.Journal.EntryDate >= periodStart
|
||||
&& l.Journal.EntryDate <= periodEnd
|
||||
&& l.Account.AccountTypeId == 5
|
||||
@@ -53,6 +54,7 @@ namespace General_Ledger_Service.Repositories
|
||||
{
|
||||
return await _context.JournalEntryLines
|
||||
.Where(l => l.Journal.IsPosted
|
||||
&& l.Journal.JournalType != "CLOSING"
|
||||
&& l.Journal.EntryDate >= periodStart
|
||||
&& l.Journal.EntryDate <= periodEnd
|
||||
&& l.AccountId == accountId)
|
||||
|
||||
Reference in New Issue
Block a user