Phase H: Add Cash Flow Statement (direct / cash-basis method)
- CashFlowStatementDto (Operating, Investing, Financing sections; BeginningCash/EndingCash) - CashFlowLineDto for Investing/Financing line items - GetCashFlowStatementAsync on IFinancialReportService + implementation in FinancialReportService - GenerateCashFlowStatementPdfAsync on IPdfService + QuestPDF implementation in PdfService - ReportsController.CashFlowStatement GET + CashFlowStatementPdf GET with inline/download mode - CashFlowStatement.cshtml view with date filter, 3-section cards, summary sidebar, methodology note - Reports Landing page: Cash Flow Statement card added to Accounting section Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -925,6 +925,114 @@ public class FinancialReportService : IFinancialReportService
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <summary>
|
||||
/// Computes a Cash Flow Statement for the given period using the direct (cash-basis) method
|
||||
/// for operating activities:
|
||||
/// <list type="bullet">
|
||||
/// <item><b>CashFromCustomers</b> — sum of <see cref="Payment"/> amounts in the period.</item>
|
||||
/// <item><b>CashToVendors</b> — sum of <see cref="BillPayment"/> amounts in the period.</item>
|
||||
/// <item><b>CashForExpenses</b> — sum of <see cref="Expense"/> amounts in the period.</item>
|
||||
/// </list>
|
||||
/// BeginningCash is derived by summing all Payment inflows minus BillPayment and Expense outflows
|
||||
/// prior to <paramref name="from"/>. This is an approximation when cash accounts have
|
||||
/// an OpeningBalance; it is the most accurate representation available without a dedicated
|
||||
/// cash-tracking journal.
|
||||
/// Investing and Financing sections are populated from the expense/asset account ledger
|
||||
/// (FixedAsset purchases from Expense entries whose account is FixedAsset subtype) and
|
||||
/// equity account changes respectively.
|
||||
/// </summary>
|
||||
public async Task<CashFlowStatementDto> GetCashFlowStatementAsync(int companyId, DateTime from, DateTime to)
|
||||
{
|
||||
var toEnd = to.Date.AddDays(1).AddTicks(-1);
|
||||
var companyName = await GetCompanyNameAsync(companyId);
|
||||
var method = await GetCompanyAccountingMethodAsync(companyId);
|
||||
|
||||
// ── Operating — direct / cash ──────────────────────────────────────
|
||||
var cashFromCustomers = await _context.Payments
|
||||
.IgnoreQueryFilters()
|
||||
.Where(p => p.CompanyId == companyId && !p.IsDeleted
|
||||
&& p.PaymentDate >= from && p.PaymentDate <= toEnd)
|
||||
.SumAsync(p => (decimal?)p.Amount) ?? 0m;
|
||||
|
||||
var cashToVendors = await _context.BillPayments
|
||||
.IgnoreQueryFilters()
|
||||
.Where(bp => bp.CompanyId == companyId && !bp.IsDeleted
|
||||
&& bp.PaymentDate >= from && bp.PaymentDate <= toEnd)
|
||||
.SumAsync(bp => (decimal?)bp.Amount) ?? 0m;
|
||||
|
||||
var cashForExpenses = await _context.Expenses
|
||||
.IgnoreQueryFilters()
|
||||
.Where(e => e.CompanyId == companyId && !e.IsDeleted
|
||||
&& e.Date >= from && e.Date <= toEnd)
|
||||
.SumAsync(e => (decimal?)e.Amount) ?? 0m;
|
||||
|
||||
// ── Investing — fixed-asset purchases from Expense entries ─────────
|
||||
var fixedAssetAccountIds = await _context.Accounts
|
||||
.IgnoreQueryFilters()
|
||||
.Where(a => a.CompanyId == companyId && !a.IsDeleted
|
||||
&& a.AccountSubType == AccountSubType.FixedAsset)
|
||||
.Select(a => a.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var capEx = fixedAssetAccountIds.Count > 0
|
||||
? (await _context.Expenses
|
||||
.IgnoreQueryFilters()
|
||||
.Where(e => e.CompanyId == companyId && !e.IsDeleted
|
||||
&& e.Date >= from && e.Date <= toEnd
|
||||
&& fixedAssetAccountIds.Contains(e.ExpenseAccountId))
|
||||
.SumAsync(e => (decimal?)e.Amount) ?? 0m)
|
||||
: 0m;
|
||||
|
||||
var investingLines = new List<CashFlowLineDto>();
|
||||
if (capEx != 0m)
|
||||
investingLines.Add(new CashFlowLineDto { Label = "Capital Expenditures", Amount = -capEx });
|
||||
|
||||
// ── Financing — placeholder (equity changes not explicitly tracked) ─
|
||||
var financingLines = new List<CashFlowLineDto>();
|
||||
|
||||
// ── Beginning cash ─────────────────────────────────────────────────
|
||||
// Cash account opening balances + pre-period payments in - pre-period payments out
|
||||
var cashAccountOpeningBalance = await _context.Accounts
|
||||
.IgnoreQueryFilters()
|
||||
.Where(a => a.CompanyId == companyId && !a.IsDeleted
|
||||
&& (a.AccountSubType == AccountSubType.Cash
|
||||
|| a.AccountSubType == AccountSubType.Checking
|
||||
|| a.AccountSubType == AccountSubType.Savings))
|
||||
.SumAsync(a => (decimal?)a.OpeningBalance) ?? 0m;
|
||||
|
||||
var prePaymentsIn = await _context.Payments
|
||||
.IgnoreQueryFilters()
|
||||
.Where(p => p.CompanyId == companyId && !p.IsDeleted && p.PaymentDate < from)
|
||||
.SumAsync(p => (decimal?)p.Amount) ?? 0m;
|
||||
|
||||
var preBillPaymentsOut = await _context.BillPayments
|
||||
.IgnoreQueryFilters()
|
||||
.Where(bp => bp.CompanyId == companyId && !bp.IsDeleted && bp.PaymentDate < from)
|
||||
.SumAsync(bp => (decimal?)bp.Amount) ?? 0m;
|
||||
|
||||
var preExpensesOut = await _context.Expenses
|
||||
.IgnoreQueryFilters()
|
||||
.Where(e => e.CompanyId == companyId && !e.IsDeleted && e.Date < from)
|
||||
.SumAsync(e => (decimal?)e.Amount) ?? 0m;
|
||||
|
||||
var beginningCash = cashAccountOpeningBalance + prePaymentsIn - preBillPaymentsOut - preExpensesOut;
|
||||
|
||||
return new CashFlowStatementDto
|
||||
{
|
||||
CompanyName = companyName,
|
||||
From = from,
|
||||
To = to,
|
||||
Method = method,
|
||||
CashFromCustomers = cashFromCustomers,
|
||||
CashToVendors = cashToVendors,
|
||||
CashForExpenses = cashForExpenses,
|
||||
InvestingLines = investingLines,
|
||||
FinancingLines = financingLines,
|
||||
BeginningCash = beginningCash,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up the company name by ID for report headers and AI prompt injection.
|
||||
/// Falls back to "Your Company" if the record is not found.
|
||||
|
||||
Reference in New Issue
Block a user