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:
2026-05-10 11:14:47 -04:00
parent 42eff3357e
commit 14026818e2
8 changed files with 546 additions and 0 deletions
@@ -6,6 +6,51 @@ namespace PowderCoating.Application.DTOs.Accounting;
// without needing a separate round-trip to the company settings.
// ── Cash Flow Statement ──────────────────────────────────────────────────────
/// <summary>
/// Cash Flow Statement using the direct (cash-basis) method for operating activities.
/// Investing and Financing sections contain line items derived from account-level changes.
/// BeginningCash + NetChangeInCash should equal EndingCash (within rounding tolerances).
/// </summary>
public class CashFlowStatementDto
{
public string CompanyName { get; set; } = string.Empty;
public DateTime From { get; set; }
public DateTime To { get; set; }
public AccountingMethod Method { get; set; }
// ── Operating (direct / cash method) ───────────────────────────────────
/// <summary>Customer invoice payments received in the period.</summary>
public decimal CashFromCustomers { get; set; }
/// <summary>Vendor bill payments made in the period.</summary>
public decimal CashToVendors { get; set; }
/// <summary>Direct expense payments made in the period (not via bills).</summary>
public decimal CashForExpenses { get; set; }
public decimal NetOperating => CashFromCustomers - CashToVendors - CashForExpenses;
// ── Investing ──────────────────────────────────────────────────────────
public List<CashFlowLineDto> InvestingLines { get; set; } = new();
public decimal NetInvesting => InvestingLines.Sum(l => l.Amount);
// ── Financing ──────────────────────────────────────────────────────────
public List<CashFlowLineDto> FinancingLines { get; set; } = new();
public decimal NetFinancing => FinancingLines.Sum(l => l.Amount);
// ── Summary ────────────────────────────────────────────────────────────
public decimal BeginningCash { get; set; }
public decimal NetChangeInCash => NetOperating + NetInvesting + NetFinancing;
public decimal EndingCash => BeginningCash + NetChangeInCash;
}
/// <summary>A single line in the Investing or Financing section of the Cash Flow Statement.</summary>
public class CashFlowLineDto
{
public string Label { get; set; } = string.Empty;
/// <summary>Positive = cash inflow, negative = cash outflow.</summary>
public decimal Amount { get; set; }
}
// ── Customer / Vendor Statements ─────────────────────────────────────────────
public class CustomerStatementDto
@@ -41,4 +41,12 @@ public interface IFinancialReportService
/// <summary>Returns a dated activity statement for a vendor showing opening balance, all transactions in the period, and closing balance.</summary>
Task<VendorStatementDto> GetVendorStatementAsync(int companyId, int vendorId, DateTime from, DateTime to);
/// <summary>
/// Returns a Cash Flow Statement for the period using the direct (cash-basis) method for
/// operating activities. Investing and Financing sections are derived from account-level data.
/// BeginningCash is computed from all cash/bank account credits and debits prior to
/// <paramref name="from"/>; EndingCash adds the net change during the period.
/// </summary>
Task<CashFlowStatementDto> GetCashFlowStatementAsync(int companyId, DateTime from, DateTime to);
}
@@ -44,6 +44,7 @@ public interface IPdfService
Task<byte[]> GenerateSalesTaxReportPdfAsync(SalesTaxReportDto dto);
Task<byte[]> GenerateApAgingPdfAsync(ApAgingReportDto dto);
Task<byte[]> GenerateTrialBalancePdfAsync(TrialBalanceDto dto);
Task<byte[]> GenerateCashFlowStatementPdfAsync(CashFlowStatementDto dto);
Task<byte[]> GenerateGiftCertificatePdfAsync(
GiftCertificateDto cert,
@@ -2593,4 +2593,120 @@ public class PdfService : IPdfService
return document.GeneratePdf();
});
}
/// <summary>
/// Generates a Cash Flow Statement PDF with three sections (Operating, Investing, Financing)
/// plus a summary reconciling beginning → ending cash. Uses a teal accent palette to
/// visually distinguish it from the other financial statements.
/// </summary>
public async Task<byte[]> GenerateCashFlowStatementPdfAsync(CashFlowStatementDto dto)
{
QuestPDF.Settings.License = LicenseType.Community;
const string accent = "#0891b2";
return await Task.Run(() =>
{
var document = Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.Letter);
page.Margin(0.6f, Unit.Inch);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x.FontSize(9).FontFamily("Arial"));
page.Header().Element(c => ComposeReportHeader(c, dto.CompanyName, "Cash Flow Statement",
$"{dto.From:MMMM d, yyyy} {dto.To:MMMM d, yyyy}", accent));
page.Content().PaddingTop(12).Column(col =>
{
col.Spacing(4);
// ── Operating Activities ──────────────────────────────────────
col.Item().Text("Operating Activities").Bold().FontSize(11).FontColor(accent);
col.Item().Table(t =>
{
t.ColumnsDefinition(c => { c.RelativeColumn(3); c.ConstantColumn(100); });
CfRow(t, "Cash Received from Customers", dto.CashFromCustomers, false);
CfRow(t, "Cash Paid to Vendors (Bills)", -dto.CashToVendors, false);
CfRow(t, "Cash Paid for Expenses", -dto.CashForExpenses, false);
CfTotalRow(t, "Net Cash from Operating Activities", dto.NetOperating);
});
col.Item().PaddingTop(10).Text("Investing Activities").Bold().FontSize(11).FontColor(accent);
col.Item().Table(t =>
{
t.ColumnsDefinition(c => { c.RelativeColumn(3); c.ConstantColumn(100); });
if (dto.InvestingLines.Count == 0)
CfRow(t, "No investing activities recorded", 0, true);
else
foreach (var line in dto.InvestingLines)
CfRow(t, line.Label, line.Amount, false);
CfTotalRow(t, "Net Cash from Investing Activities", dto.NetInvesting);
});
col.Item().PaddingTop(10).Text("Financing Activities").Bold().FontSize(11).FontColor(accent);
col.Item().Table(t =>
{
t.ColumnsDefinition(c => { c.RelativeColumn(3); c.ConstantColumn(100); });
if (dto.FinancingLines.Count == 0)
CfRow(t, "No financing activities recorded", 0, true);
else
foreach (var line in dto.FinancingLines)
CfRow(t, line.Label, line.Amount, false);
CfTotalRow(t, "Net Cash from Financing Activities", dto.NetFinancing);
});
// ── Summary ───────────────────────────────────────────────────
col.Item().PaddingTop(12).Table(t =>
{
t.ColumnsDefinition(c => { c.RelativeColumn(3); c.ConstantColumn(100); });
void SumRow(string label, decimal amount, bool bold = false)
{
var bg = bold ? "#e0f2fe" : "#ffffff";
var lText = t.Cell().Background(bg).PaddingVertical(4).PaddingHorizontal(6).Text(label).FontSize(9);
if (bold) lText.Bold();
var vText = t.Cell().Background(bg).PaddingVertical(4).PaddingHorizontal(6).AlignRight()
.Text(amount.ToString("C")).FontSize(9)
.FontColor(amount < 0 ? Colors.Red.Darken2 : Colors.Black);
if (bold) vText.Bold();
}
SumRow("Beginning Cash Balance", dto.BeginningCash);
SumRow("Net Change in Cash", dto.NetChangeInCash);
SumRow("Ending Cash Balance", dto.EndingCash, bold: true);
});
});
page.Footer().AlignCenter().Text(text =>
{
text.CurrentPageNumber(); text.Span(" / "); text.TotalPages();
text.Span($" · {dto.CompanyName} · Generated {DateTime.Now:MMM d, yyyy}").FontSize(8).FontColor(Colors.Grey.Darken1);
});
});
});
return document.GeneratePdf();
});
static void CfRow(TableDescriptor t, string label, decimal amount, bool muted)
{
t.Cell().BorderBottom(0.5f).BorderColor("#e5e7eb")
.PaddingVertical(3).PaddingHorizontal(6)
.Text(label).FontSize(9).FontColor(muted ? Colors.Grey.Medium : Colors.Black);
t.Cell().BorderBottom(0.5f).BorderColor("#e5e7eb")
.PaddingVertical(3).PaddingHorizontal(6).AlignRight()
.Text(muted ? "" : amount.ToString("C")).FontSize(9)
.FontColor(amount < 0 ? Colors.Red.Darken2 : Colors.Black);
}
static void CfTotalRow(TableDescriptor t, string label, decimal amount)
{
t.Cell().Background("#f0f9ff").PaddingVertical(4).PaddingHorizontal(6)
.Text(label).Bold().FontSize(9);
t.Cell().Background("#f0f9ff").PaddingVertical(4).PaddingHorizontal(6).AlignRight()
.Text(amount.ToString("C")).Bold().FontSize(9)
.FontColor(amount < 0 ? Colors.Red.Darken2 : Colors.Black);
}
}
}
@@ -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.
@@ -1255,6 +1255,42 @@ public class ReportsController : Controller
: File(pdfBytes, "application/pdf", $"TrialBalance-{asOfDate:yyyyMMdd}.pdf");
}
/// <summary>
/// Cash Flow Statement — shows cash receipts from customers, cash payments to vendors and
/// for direct expenses, and a summary of beginning/ending cash position. Uses the direct
/// (cash-basis) method for operating activities so the numbers reflect actual cash movement
/// regardless of the company's accrual vs cash accounting preference.
/// Gated behind <see cref="AllowAccounting"/>.
/// </summary>
// GET: /Reports/CashFlowStatement
public async Task<IActionResult> CashFlowStatement(DateTime? from, DateTime? to)
{
if (!AllowAccounting()) return RedirectToAction(nameof(Landing));
var fromDate = (from ?? new DateTime(DateTime.Today.Year, 1, 1)).Date;
var toDate = (to ?? DateTime.Today).Date;
var companyId = int.TryParse(User.FindFirst("CompanyId")?.Value, out var cid) ? cid : 0;
var dto = await _financialReports.GetCashFlowStatementAsync(companyId, fromDate, toDate);
return View(dto);
}
/// <summary>
/// PDF export of the Cash Flow Statement. Same inline/attachment pattern as other PDF actions.
/// Gated behind <see cref="AllowAccounting"/>.
/// </summary>
// GET: /Reports/CashFlowStatementPdf
public async Task<IActionResult> CashFlowStatementPdf(DateTime? from, DateTime? to, bool inline = false)
{
if (!AllowAccounting()) return RedirectToAction(nameof(Landing));
var fromDate = (from ?? new DateTime(DateTime.Today.Year, 1, 1)).Date;
var toDate = (to ?? DateTime.Today).Date;
var companyId = int.TryParse(User.FindFirst("CompanyId")?.Value, out var cid) ? cid : 0;
var dto = await _financialReports.GetCashFlowStatementAsync(companyId, fromDate, toDate);
var pdfBytes = await _pdfService.GenerateCashFlowStatementPdfAsync(dto);
return inline
? File(pdfBytes, "application/pdf")
: File(pdfBytes, "application/pdf", $"CashFlow-{fromDate:yyyyMMdd}-{toDate:yyyyMMdd}.pdf");
}
// ── INDIVIDUAL REPORT PAGES ──────────────────────────────────────────────
/// <summary>
@@ -0,0 +1,224 @@
@model PowderCoating.Application.DTOs.Accounting.CashFlowStatementDto
@using PowderCoating.Core.Enums
@{
ViewData["Title"] = "Cash Flow Statement";
string AmountClass(decimal v) => v < 0 ? "text-danger" : "text-body";
string Fmt(decimal v) => v.ToString("C");
}
<div class="d-flex align-items-center justify-content-between mb-4">
<div>
<h4 class="fw-bold mb-0"><i class="bi bi-water me-2 text-info"></i>Cash Flow Statement</h4>
<p class="text-muted small mb-0">
@Model.From.ToString("MMMM d, yyyy") @Model.To.ToString("MMMM d, yyyy")
&nbsp;·&nbsp; Direct Method (Cash Basis)
</p>
</div>
<div class="d-flex gap-2">
<a asp-action="CashFlowStatementPdf"
asp-route-from="@Model.From.ToString("yyyy-MM-dd")"
asp-route-to="@Model.To.ToString("yyyy-MM-dd")"
class="btn btn-outline-secondary btn-sm">
<i class="bi bi-download me-1"></i>PDF
</a>
<a asp-action="CashFlowStatementPdf"
asp-route-from="@Model.From.ToString("yyyy-MM-dd")"
asp-route-to="@Model.To.ToString("yyyy-MM-dd")"
asp-route-inline="true"
target="_blank"
class="btn btn-outline-primary btn-sm">
<i class="bi bi-eye me-1"></i>Preview
</a>
</div>
</div>
<!-- Date range filter -->
<form method="get" asp-action="CashFlowStatement" class="card shadow-sm mb-4">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-auto">
<label class="form-label fw-semibold small">From</label>
<input type="date" name="from" class="form-control form-control-sm" value="@Model.From.ToString("yyyy-MM-dd")" />
</div>
<div class="col-auto">
<label class="form-label fw-semibold small">To</label>
<input type="date" name="to" class="form-control form-control-sm" value="@Model.To.ToString("yyyy-MM-dd")" />
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-primary">Update</button>
</div>
<!-- Quick date presets -->
@{
var y = DateTime.Today.Year;
var presets = new[]
{
("YTD", new DateTime(y, 1, 1).ToString("yyyy-MM-dd"), DateTime.Today.ToString("yyyy-MM-dd")),
("This Qtr", new DateTime(y, ((DateTime.Today.Month - 1) / 3) * 3 + 1, 1).ToString("yyyy-MM-dd"), DateTime.Today.ToString("yyyy-MM-dd")),
("Last Year", new DateTime(y-1, 1, 1).ToString("yyyy-MM-dd"), new DateTime(y-1, 12, 31).ToString("yyyy-MM-dd")),
};
}
@foreach (var (label, f, t) in presets)
{
<div class="col-auto">
<a asp-action="CashFlowStatement" asp-route-from="@f" asp-route-to="@t"
class="btn btn-sm btn-outline-secondary">@label</a>
</div>
}
</div>
</div>
</form>
<div class="row g-4">
<!-- Main statement -->
<div class="col-lg-8">
<!-- Operating Activities -->
<div class="card shadow-sm mb-3">
<div class="card-header d-flex align-items-center justify-content-between">
<span class="fw-semibold"><i class="bi bi-gear me-2 text-info"></i>Operating Activities</span>
<span class="badge @(Model.NetOperating >= 0 ? "bg-success" : "bg-danger")">@Fmt(Model.NetOperating)</span>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0">
<tbody>
<tr>
<td class="ps-3 text-body-secondary">Cash received from customers</td>
<td class="text-end pe-3 text-success fw-semibold">@Fmt(Model.CashFromCustomers)</td>
</tr>
<tr>
<td class="ps-3 text-body-secondary">Cash paid to vendors (bills)</td>
<td class="text-end pe-3 @AmountClass(-Model.CashToVendors)">(@Fmt(Model.CashToVendors))</td>
</tr>
<tr>
<td class="ps-3 text-body-secondary">Cash paid for direct expenses</td>
<td class="text-end pe-3 @AmountClass(-Model.CashForExpenses)">(@Fmt(Model.CashForExpenses))</td>
</tr>
</tbody>
<tfoot class="table-light">
<tr>
<td class="ps-3 fw-semibold">Net Cash from Operating Activities</td>
<td class="text-end pe-3 fw-bold @AmountClass(Model.NetOperating)">@Fmt(Model.NetOperating)</td>
</tr>
</tfoot>
</table>
</div>
</div>
<!-- Investing Activities -->
<div class="card shadow-sm mb-3">
<div class="card-header d-flex align-items-center justify-content-between">
<span class="fw-semibold"><i class="bi bi-building me-2 text-primary"></i>Investing Activities</span>
<span class="badge @(Model.NetInvesting >= 0 ? "bg-success" : "bg-danger")">@Fmt(Model.NetInvesting)</span>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0">
<tbody>
@if (!Model.InvestingLines.Any())
{
<tr>
<td class="ps-3 text-muted" colspan="2">
<i class="bi bi-dash-circle me-1"></i>No investing activities recorded in this period.
</td>
</tr>
}
else
{
@foreach (var line in Model.InvestingLines)
{
<tr>
<td class="ps-3 text-body-secondary">@line.Label</td>
<td class="text-end pe-3 @AmountClass(line.Amount)">@Fmt(line.Amount)</td>
</tr>
}
}
</tbody>
<tfoot class="table-light">
<tr>
<td class="ps-3 fw-semibold">Net Cash from Investing Activities</td>
<td class="text-end pe-3 fw-bold @AmountClass(Model.NetInvesting)">@Fmt(Model.NetInvesting)</td>
</tr>
</tfoot>
</table>
</div>
</div>
<!-- Financing Activities -->
<div class="card shadow-sm mb-3">
<div class="card-header d-flex align-items-center justify-content-between">
<span class="fw-semibold"><i class="bi bi-bank me-2 text-secondary"></i>Financing Activities</span>
<span class="badge @(Model.NetFinancing >= 0 ? "bg-success" : "bg-danger")">@Fmt(Model.NetFinancing)</span>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0">
<tbody>
@if (!Model.FinancingLines.Any())
{
<tr>
<td class="ps-3 text-muted" colspan="2">
<i class="bi bi-dash-circle me-1"></i>No financing activities recorded in this period.
</td>
</tr>
}
else
{
@foreach (var line in Model.FinancingLines)
{
<tr>
<td class="ps-3 text-body-secondary">@line.Label</td>
<td class="text-end pe-3 @AmountClass(line.Amount)">@Fmt(line.Amount)</td>
</tr>
}
}
</tbody>
<tfoot class="table-light">
<tr>
<td class="ps-3 fw-semibold">Net Cash from Financing Activities</td>
<td class="text-end pe-3 fw-bold @AmountClass(Model.NetFinancing)">@Fmt(Model.NetFinancing)</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<!-- Summary sidebar -->
<div class="col-lg-4">
<div class="card shadow-sm mb-3">
<div class="card-header fw-semibold"><i class="bi bi-calculator me-2 text-info"></i>Cash Summary</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-8 text-muted small fw-normal">Beginning Cash</dt>
<dd class="col-4 text-end fw-semibold mb-2">@Fmt(Model.BeginningCash)</dd>
<dt class="col-8 text-muted small fw-normal">Operating</dt>
<dd class="col-4 text-end fw-semibold mb-1 @AmountClass(Model.NetOperating)">@Fmt(Model.NetOperating)</dd>
<dt class="col-8 text-muted small fw-normal">Investing</dt>
<dd class="col-4 text-end fw-semibold mb-1 @AmountClass(Model.NetInvesting)">@Fmt(Model.NetInvesting)</dd>
<dt class="col-8 text-muted small fw-normal">Financing</dt>
<dd class="col-4 text-end fw-semibold mb-2 @AmountClass(Model.NetFinancing)">@Fmt(Model.NetFinancing)</dd>
<dt class="col-8 text-muted small fw-normal">Net Change in Cash</dt>
<dd class="col-4 text-end fw-semibold mb-3 @AmountClass(Model.NetChangeInCash)">@Fmt(Model.NetChangeInCash)</dd>
<dt class="col-8 fw-bold">Ending Cash Balance</dt>
<dd class="col-4 text-end fw-bold fs-5 @AmountClass(Model.EndingCash)">@Fmt(Model.EndingCash)</dd>
</dl>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header fw-semibold small"><i class="bi bi-info-circle me-2"></i>Methodology</div>
<div class="card-body small text-muted">
<p class="mb-2">This statement uses the <strong>direct (cash basis)</strong> method for Operating Activities:</p>
<ul class="mb-2 ps-3">
<li>Inflows = customer invoice payments received</li>
<li>Outflows = vendor bill payments + direct expense payments</li>
</ul>
<p class="mb-0">Beginning Cash is approximated from all cash inflows and outflows recorded prior to the start date plus account opening balances. For the most accurate beginning balance, reconcile your bank accounts first.</p>
</div>
</div>
</div>
</div>
@@ -204,6 +204,14 @@
<p>All active accounts with debit and credit balances — validates that your books are in balance.</p>
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
</a>
<a asp-controller="Reports" asp-action="CashFlowStatement" class="report-card">
<div class="report-card-icon" style="background:#ecfeff;color:#0891b2;">
<i class="bi bi-water"></i>
</div>
<h5>Cash Flow Statement</h5>
<p>Track actual cash in/out across operating, investing, and financing activities with beginning and ending cash balance.</p>
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
</a>
</div>
</div>
}