Add Phase A accounting features: AP Aging, Trial Balance, Cash vs Accrual

- AP Aging report (GetApAgingAsync, controller actions, view, PDF export)
  mirrors AR Aging — groups open bills by vendor, buckets by days past due date
- Trial Balance report (GetTrialBalanceAsync, view, PDF export)
  uses Account.CurrentBalance, groups by AccountType, validates debits == credits
- Cash vs Accrual accounting method setting on Company entity
  switchable at any time — report-time only, no GL re-posting on change
  P&L cash: revenue = payments received; expenses = bills/expenses paid in period
  Balance Sheet cash: omits AR and AP lines (no receivables/payables concept)
  AccountingMethod badge shown on P&L and Balance Sheet views
- Migration A (AddAccountingMethod) applied, default = Accrual for all existing companies
- AP Aging and Trial Balance added to Reports Landing page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-09 23:34:54 -04:00
parent 379b0de885
commit 7e1676cfd7
18 changed files with 10765 additions and 67 deletions
@@ -1192,6 +1192,69 @@ public class ReportsController : Controller
return File(bytes, "text/csv", $"SalesTaxReport-{fromDate:yyyyMMdd}-{toDate:yyyyMMdd}.csv");
}
/// <summary>
/// Accounts Payable Aging report — mirrors the AR Aging but groups open bills by vendor
/// and buckets them by days past due date. Gated behind <see cref="AllowAccounting"/>.
/// </summary>
// GET: /Reports/ApAging
public async Task<IActionResult> ApAging(DateTime? asOf)
{
if (!AllowAccounting()) return RedirectToAction(nameof(Landing));
var asOfDate = (asOf ?? DateTime.Today).Date;
var companyId = int.TryParse(User.FindFirst("CompanyId")?.Value, out var cid) ? cid : 0;
var dto = await _financialReports.GetApAgingAsync(companyId, asOfDate);
return View(dto);
}
/// <summary>
/// PDF export of the AP Aging report. Same inline/attachment pattern as other PDF actions.
/// Gated behind <see cref="AllowAccounting"/>.
/// </summary>
// GET: /Reports/ApAgingPdf
public async Task<IActionResult> ApAgingPdf(DateTime? asOf, bool inline = false)
{
if (!AllowAccounting()) return RedirectToAction(nameof(Landing));
var asOfDate = (asOf ?? DateTime.Today).Date;
var companyId = int.TryParse(User.FindFirst("CompanyId")?.Value, out var cid) ? cid : 0;
var dto = await _financialReports.GetApAgingAsync(companyId, asOfDate);
var pdfBytes = await _pdfService.GenerateApAgingPdfAsync(dto);
return inline
? File(pdfBytes, "application/pdf")
: File(pdfBytes, "application/pdf", $"AP-Aging-{asOfDate:yyyyMMdd}.pdf");
}
/// <summary>
/// Trial Balance report — lists all active accounts with debit and credit balances using
/// <c>Account.CurrentBalance</c> (live, not point-in-time). Validates that debits equal
/// credits. Gated behind <see cref="AllowAccounting"/>.
/// </summary>
// GET: /Reports/TrialBalance
public async Task<IActionResult> TrialBalance(DateTime? asOf)
{
if (!AllowAccounting()) return RedirectToAction(nameof(Landing));
var asOfDate = (asOf ?? DateTime.Today).Date;
var companyId = int.TryParse(User.FindFirst("CompanyId")?.Value, out var cid) ? cid : 0;
var dto = await _financialReports.GetTrialBalanceAsync(companyId, asOfDate);
return View(dto);
}
/// <summary>
/// PDF export of the Trial Balance report. Same inline/attachment pattern as other PDF actions.
/// Gated behind <see cref="AllowAccounting"/>.
/// </summary>
// GET: /Reports/TrialBalancePdf
public async Task<IActionResult> TrialBalancePdf(DateTime? asOf, bool inline = false)
{
if (!AllowAccounting()) return RedirectToAction(nameof(Landing));
var asOfDate = (asOf ?? DateTime.Today).Date;
var companyId = int.TryParse(User.FindFirst("CompanyId")?.Value, out var cid) ? cid : 0;
var dto = await _financialReports.GetTrialBalanceAsync(companyId, asOfDate);
var pdfBytes = await _pdfService.GenerateTrialBalancePdfAsync(dto);
return inline
? File(pdfBytes, "application/pdf")
: File(pdfBytes, "application/pdf", $"TrialBalance-{asOfDate:yyyyMMdd}.pdf");
}
// ── INDIVIDUAL REPORT PAGES ──────────────────────────────────────────────
/// <summary>
@@ -199,6 +199,16 @@
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="accountingMethod" class="form-label">Accounting Method</label>
<select class="form-select" id="accountingMethod" name="AccountingMethod">
<option value="1" selected="@(Model.AccountingMethod == PowderCoating.Core.Enums.AccountingMethod.Accrual ? "selected" : null)">Accrual (default)</option>
<option value="0" selected="@(Model.AccountingMethod == PowderCoating.Core.Enums.AccountingMethod.Cash ? "selected" : null)">Cash Basis</option>
</select>
<div class="form-text">Affects how financial reports (P&amp;L, Balance Sheet, Cash Flow) present data. Switching does not re-post historical transactions.</div>
</div>
</div>
</div>
<div class="mb-3">
@@ -2166,7 +2176,8 @@
City: $('#city').val(),
State: $('#state').val(),
ZipCode: $('#zipCode').val(),
TimeZone: $('#timeZone').val()
TimeZone: $('#timeZone').val(),
AccountingMethod: parseInt($('#accountingMethod').val())
};
const btn = $('#btnSaveCompanyInfo');
@@ -0,0 +1,242 @@
@model PowderCoating.Application.DTOs.Accounting.ApAgingReportDto
@{
ViewData["Title"] = "AP Aging";
ViewData["PageIcon"] = "bi-hourglass-split";
var today = DateTime.Today;
}
<style>
@@media print {
.no-print { display: none !important; }
.card { border: 1px solid #dee2e6 !important; box-shadow: none !important; }
body { font-size: 11px; }
.table { font-size: 11px; }
}
.aging-current { color: #198754; }
.aging-1-30 { color: #fd7e14; }
.aging-31-60 { color: #dc6c02; }
.aging-61-90 { color: #dc3545; }
.aging-over90 { color: #842029; font-weight: 700; }
.vendor-row { background: #f8f9fa; font-weight: 600; }
</style>
<!-- Header -->
<div class="d-flex align-items-center gap-2 mb-3 no-print">
<a asp-action="Index" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left"></i></a>
<p class="text-muted mb-0">As of @Model.AsOf.ToString("MMMM d, yyyy") · @Model.Vendors.Sum(v => v.Bills.Count) open bills</p>
<div class="ms-auto d-flex gap-2">
<a href="@Url.Action("ApAgingPdf", new { asOf = Model.AsOf.ToString("yyyy-MM-dd") })"
class="btn btn-sm btn-outline-danger no-print" target="_blank">
<i class="bi bi-file-pdf me-1"></i>Download PDF
</a>
<a href="@Url.Action("ApAgingPdf", new { asOf = Model.AsOf.ToString("yyyy-MM-dd"), inline = true })"
class="btn btn-sm btn-outline-secondary no-print" target="_blank">
<i class="bi bi-printer me-1"></i>Print
</a>
</div>
</div>
<!-- Date filter -->
<div class="card shadow-sm mb-4 no-print">
<div class="card-body py-3">
<form method="get" class="row g-2 align-items-end">
<div class="col-auto">
<label class="form-label form-label-sm mb-1">As of Date</label>
<input type="date" name="asOf" class="form-control form-control-sm" value="@Model.AsOf.ToString("yyyy-MM-dd")" />
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-funnel me-1"></i>Run Report</button>
</div>
<div class="col-auto ms-2">
<div class="btn-group btn-group-sm">
<a href="@Url.Action("ApAging", new { asOf = today.ToString("yyyy-MM-dd") })" class="btn btn-outline-secondary">Today</a>
<a href="@Url.Action("ApAging", new { asOf = new DateTime(today.Year, today.Month, 1).AddDays(-1).ToString("yyyy-MM-dd") })" class="btn btn-outline-secondary">End of Last Month</a>
</div>
</div>
</form>
</div>
</div>
<!-- Print header -->
<div class="text-center mb-4 d-none d-print-block">
<h4 class="fw-bold">@Model.CompanyName</h4>
<h5>Accounts Payable Aging</h5>
<p class="text-muted">As of @Model.AsOf.ToString("MMMM d, yyyy")</p>
</div>
<!-- Aging summary cards -->
<div class="row g-3 mb-4">
<div class="col-6 col-lg">
<div class="card shadow-sm text-center h-100">
<div class="card-body py-3">
<div class="h6 text-success mb-1">@Model.TotalCurrent.ToString("C0")</div>
<div class="text-muted small">Current</div>
</div>
</div>
</div>
<div class="col-6 col-lg">
<div class="card shadow-sm text-center h-100">
<div class="card-body py-3">
<div class="h6 aging-1-30 mb-1">@Model.Total1to30.ToString("C0")</div>
<div class="text-muted small">130 Days</div>
</div>
</div>
</div>
<div class="col-6 col-lg">
<div class="card shadow-sm text-center h-100">
<div class="card-body py-3">
<div class="h6 aging-31-60 mb-1">@Model.Total31to60.ToString("C0")</div>
<div class="text-muted small">3160 Days</div>
</div>
</div>
</div>
<div class="col-6 col-lg">
<div class="card shadow-sm text-center h-100">
<div class="card-body py-3">
<div class="h6 aging-61-90 mb-1">@Model.Total61to90.ToString("C0")</div>
<div class="text-muted small">6190 Days</div>
</div>
</div>
</div>
<div class="col-6 col-lg">
<div class="card shadow-sm text-center h-100">
<div class="card-body py-3">
<div class="h6 aging-over90 mb-1">@Model.TotalOver90.ToString("C0")</div>
<div class="text-muted small">Over 90 Days</div>
</div>
</div>
</div>
<div class="col-6 col-lg">
<div class="card shadow-sm text-center h-100 border-danger border-opacity-25">
<div class="card-body py-3">
<div class="h6 text-danger fw-bold mb-1">@Model.TotalOutstanding.ToString("C0")</div>
<div class="text-muted small">Total Owed</div>
</div>
</div>
</div>
</div>
@if (!Model.Vendors.Any())
{
<div class="card shadow-sm">
<div class="card-body text-center py-5 text-muted">
<i class="bi bi-check-circle text-success fs-1 d-block mb-2"></i>
<p class="mb-0 fw-semibold">All bills are paid!</p>
<p class="small mb-0">No outstanding balances as of @Model.AsOf.ToString("MMMM d, yyyy").</p>
</div>
</div>
}
else
{
<!-- Summary table -->
<div class="card shadow-sm mb-4">
<div class="card-header fw-semibold">
<i class="bi bi-table me-1"></i>Aging Summary by Vendor
</div>
<div class="table-responsive">
<table class="table table-hover table-sm align-middle mb-0">
<thead class="table-light">
<tr>
<th>Vendor</th>
<th class="text-end">Current</th>
<th class="text-end">130 Days</th>
<th class="text-end">3160 Days</th>
<th class="text-end">6190 Days</th>
<th class="text-end">Over 90</th>
<th class="text-end">Total</th>
</tr>
</thead>
<tbody>
@foreach (var vend in Model.Vendors)
{
<tr>
<td>
<a asp-controller="Vendors" asp-action="Details" asp-route-id="@vend.VendorId" class="text-decoration-none fw-medium">
@vend.VendorName
</a>
<span class="badge bg-secondary ms-1">@vend.Bills.Count bill@(vend.Bills.Count == 1 ? "" : "s")</span>
</td>
<td class="text-end aging-current">@(vend.TotalCurrent > 0 ? vend.TotalCurrent.ToString("C") : "—")</td>
<td class="text-end aging-1-30">@(vend.Total1to30 > 0 ? vend.Total1to30.ToString("C") : "—")</td>
<td class="text-end aging-31-60">@(vend.Total31to60 > 0 ? vend.Total31to60.ToString("C") : "—")</td>
<td class="text-end aging-61-90">@(vend.Total61to90 > 0 ? vend.Total61to90.ToString("C") : "—")</td>
<td class="text-end aging-over90">@(vend.TotalOver90 > 0 ? vend.TotalOver90.ToString("C") : "—")</td>
<td class="text-end fw-semibold">@vend.TotalBalance.ToString("C")</td>
</tr>
}
</tbody>
<tfoot class="table-light fw-bold">
<tr>
<td>Total</td>
<td class="text-end aging-current">@Model.TotalCurrent.ToString("C")</td>
<td class="text-end aging-1-30">@Model.Total1to30.ToString("C")</td>
<td class="text-end aging-31-60">@Model.Total31to60.ToString("C")</td>
<td class="text-end aging-61-90">@Model.Total61to90.ToString("C")</td>
<td class="text-end aging-over90">@Model.TotalOver90.ToString("C")</td>
<td class="text-end">@Model.TotalOutstanding.ToString("C")</td>
</tr>
</tfoot>
</table>
</div>
</div>
<!-- Detail by vendor -->
<div class="card shadow-sm">
<div class="card-header fw-semibold">
<i class="bi bi-list-ul me-1"></i>Bill Detail
</div>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead class="table-light">
<tr>
<th>Bill #</th>
<th>Bill Date</th>
<th>Due Date</th>
<th class="text-end">Balance Due</th>
<th class="text-end">Age</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var vend in Model.Vendors)
{
<tr class="vendor-row">
<td colspan="6" class="py-2">@vend.VendorName</td>
</tr>
@foreach (var bill in vend.Bills.OrderBy(b => b.DaysOverdue))
{
string ageBadge = bill.DaysOverdue <= 0 ? "bg-success-subtle text-success"
: bill.DaysOverdue <= 30 ? "bg-warning-subtle text-warning"
: bill.DaysOverdue <= 60 ? "bg-orange-subtle text-warning"
: bill.DaysOverdue <= 90 ? "bg-danger-subtle text-danger"
: "bg-danger text-white";
string ageLabel = bill.DaysOverdue <= 0 ? "Current" : $"{bill.DaysOverdue}d overdue";
<tr>
<td class="ps-4">
<a asp-controller="Bills" asp-action="Details" asp-route-id="@bill.BillId" class="text-decoration-none fw-medium">
@bill.BillNumber
</a>
</td>
<td class="text-muted small">@bill.BillDate.ToString("MM/dd/yyyy")</td>
<td class="text-muted small">@(bill.DueDate?.ToString("MM/dd/yyyy") ?? "—")</td>
<td class="text-end fw-semibold @(bill.DaysOverdue > 30 ? "text-danger" : "")">@bill.BalanceDue.ToString("C")</td>
<td class="text-end"><span class="badge @ageBadge">@ageLabel</span></td>
<td></td>
</tr>
}
<tr class="table-light">
<td colspan="3" class="ps-4 fw-semibold text-end small">@vend.VendorName subtotal</td>
<td class="text-end fw-semibold">@vend.TotalBalance.ToString("C")</td>
<td colspan="2"></td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
<div class="text-muted small mt-2 no-print">
<i class="bi bi-info-circle me-1"></i>
Generated @DateTime.Now.ToString("MMM d, yyyy h:mm tt") · Includes all open bills (excluding Draft and Voided). Age calculated from due date.
</div>
@@ -22,6 +22,14 @@
<div class="d-flex align-items-center gap-2 mb-3 no-print">
<a asp-action="Index" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left"></i></a>
<p class="text-muted mb-0">As of @Model.AsOf.ToString("MMMM d, yyyy")</p>
@if (Model.AccountingMethod == PowderCoating.Core.Enums.AccountingMethod.Cash)
{
<span class="badge bg-warning text-dark">Cash Basis</span>
}
else
{
<span class="badge bg-info text-dark">Accrual Basis</span>
}
<div class="ms-auto d-flex gap-2">
<a href="@Url.Action("BalanceSheetPdf", new { asOf = Model.AsOf.ToString("yyyy-MM-dd") })"
class="btn btn-sm btn-outline-danger no-print" target="_blank">
@@ -188,6 +188,22 @@
<p>Snapshot of assets, liabilities, and equity as of any date.</p>
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
</a>
<a asp-controller="Reports" asp-action="ApAging" class="report-card">
<div class="report-card-icon" style="background:#fff1f2;color:#b91c1c;">
<i class="bi bi-hourglass-split"></i>
</div>
<h5>AP Aging</h5>
<p>Outstanding vendor bills by age — current, 30, 60, and 90+ days past due. Exportable to PDF.</p>
<div class="report-arrow">Open report <i class="bi bi-arrow-right"></i></div>
</a>
<a asp-controller="Reports" asp-action="TrialBalance" class="report-card">
<div class="report-card-icon" style="background:#eef2ff;color:#4338ca;">
<i class="bi bi-list-columns-reverse"></i>
</div>
<h5>Trial Balance</h5>
<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>
</div>
</div>
}
@@ -30,6 +30,14 @@
<div class="d-flex align-items-center gap-2 mb-3 no-print">
<a asp-action="Index" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left"></i></a>
<p class="text-muted mb-0">Income Statement — @Model.From.ToString("MMM d") @Model.To.ToString("MMM d, yyyy")</p>
@if (Model.AccountingMethod == PowderCoating.Core.Enums.AccountingMethod.Cash)
{
<span class="badge bg-warning text-dark">Cash Basis</span>
}
else
{
<span class="badge bg-info text-dark">Accrual Basis</span>
}
<div class="ms-auto d-flex gap-2">
<a href="@Url.Action("ProfitAndLossPdf", new { from = Model.From.ToString("yyyy-MM-dd"), to = Model.To.ToString("yyyy-MM-dd") })"
class="btn btn-sm btn-outline-danger no-print" target="_blank">
@@ -0,0 +1,183 @@
@model PowderCoating.Application.DTOs.Accounting.TrialBalanceDto
@using PowderCoating.Core.Enums
@{
ViewData["Title"] = "Trial Balance";
ViewData["PageIcon"] = "bi-list-columns-reverse";
var today = DateTime.Today;
var grouped = Model.Lines.GroupBy(l => l.AccountType).OrderBy(g => g.Key.ToString());
}
<style>
@@media print {
.no-print { display: none !important; }
.card { border: 1px solid #dee2e6 !important; box-shadow: none !important; }
body { font-size: 11px; }
.table { font-size: 11px; }
}
.type-header { background: #f1f5f9; font-weight: 600; }
</style>
<!-- Header -->
<div class="d-flex align-items-center gap-2 mb-3 no-print">
<a asp-action="Index" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left"></i></a>
<p class="text-muted mb-0">
As of @Model.AsOf.ToString("MMMM d, yyyy") ·
@if (Model.IsBalanced)
{
<span class="text-success fw-semibold"><i class="bi bi-check-circle me-1"></i>Balanced</span>
}
else
{
<span class="text-danger fw-semibold"><i class="bi bi-exclamation-triangle me-1"></i>Out of Balance by @((Model.TotalDebits - Model.TotalCredits).ToString("C"))</span>
}
</p>
<div class="ms-auto d-flex gap-2">
<a href="@Url.Action("TrialBalancePdf", new { asOf = Model.AsOf.ToString("yyyy-MM-dd") })"
class="btn btn-sm btn-outline-danger no-print" target="_blank">
<i class="bi bi-file-pdf me-1"></i>Download PDF
</a>
<a href="@Url.Action("TrialBalancePdf", new { asOf = Model.AsOf.ToString("yyyy-MM-dd"), inline = true })"
class="btn btn-sm btn-outline-secondary no-print" target="_blank">
<i class="bi bi-printer me-1"></i>Print
</a>
</div>
</div>
<!-- Date filter -->
<div class="card shadow-sm mb-4 no-print">
<div class="card-body py-3">
<form method="get" class="row g-2 align-items-end">
<div class="col-auto">
<label class="form-label form-label-sm mb-1">As of Date</label>
<input type="date" name="asOf" class="form-control form-control-sm" value="@Model.AsOf.ToString("yyyy-MM-dd")" />
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-funnel me-1"></i>Run Report</button>
</div>
<div class="col-auto ms-2">
<div class="btn-group btn-group-sm">
<a href="@Url.Action("TrialBalance", new { asOf = today.ToString("yyyy-MM-dd") })" class="btn btn-outline-secondary">Today</a>
<a href="@Url.Action("TrialBalance", new { asOf = new DateTime(today.Year, today.Month, 1).AddDays(-1).ToString("yyyy-MM-dd") })" class="btn btn-outline-secondary">End of Last Month</a>
</div>
</div>
</form>
</div>
</div>
<!-- Print header -->
<div class="text-center mb-4 d-none d-print-block">
<h4 class="fw-bold">@Model.CompanyName</h4>
<h5>Trial Balance</h5>
<p class="text-muted">As of @Model.AsOf.ToString("MMMM d, yyyy")</p>
</div>
<!-- Summary cards -->
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="card shadow-sm text-center h-100">
<div class="card-body py-3">
<div class="h5 text-primary fw-bold mb-1">@Model.TotalDebits.ToString("C0")</div>
<div class="text-muted small">Total Debits</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm text-center h-100">
<div class="card-body py-3">
<div class="h5 text-primary fw-bold mb-1">@Model.TotalCredits.ToString("C0")</div>
<div class="text-muted small">Total Credits</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm text-center h-100 @(Model.IsBalanced ? "border-success border-opacity-50" : "border-danger border-opacity-50")">
<div class="card-body py-3">
@if (Model.IsBalanced)
{
<div class="h5 text-success fw-bold mb-1"><i class="bi bi-check-circle me-1"></i>Balanced</div>
<div class="text-muted small">Debits = Credits</div>
}
else
{
<div class="h5 text-danger fw-bold mb-1"><i class="bi bi-exclamation-triangle me-1"></i>Unbalanced</div>
<div class="text-muted small">Difference: @((Model.TotalDebits - Model.TotalCredits).ToString("C"))</div>
}
</div>
</div>
</div>
</div>
@if (!Model.Lines.Any())
{
<div class="card shadow-sm">
<div class="card-body text-center py-5 text-muted">
<i class="bi bi-journal-x fs-1 d-block mb-2"></i>
<p class="mb-0 fw-semibold">No active accounts with balances found.</p>
</div>
</div>
}
else
{
<div class="card shadow-sm">
<div class="card-header fw-semibold">
<i class="bi bi-list-columns-reverse me-1"></i>Account Balances
</div>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead class="table-light">
<tr>
<th style="width:90px">Acct #</th>
<th>Account Name</th>
<th class="text-end" style="width:140px">Debit</th>
<th class="text-end" style="width:140px">Credit</th>
</tr>
</thead>
<tbody>
@foreach (var grp in grouped)
{
<tr class="type-header">
<td colspan="4" class="py-2 text-uppercase small tracking-wide">@grp.Key</td>
</tr>
@foreach (var line in grp.OrderBy(l => l.AccountNumber))
{
<tr>
<td class="ps-4 text-muted small">@line.AccountNumber</td>
<td>@line.AccountName</td>
<td class="text-end @(line.DebitBalance > 0 ? "fw-medium" : "text-muted")">
@(line.DebitBalance > 0 ? line.DebitBalance.ToString("C") : "")
</td>
<td class="text-end @(line.CreditBalance > 0 ? "fw-medium" : "text-muted")">
@(line.CreditBalance > 0 ? line.CreditBalance.ToString("C") : "")
</td>
</tr>
}
<tr class="table-light">
<td colspan="2" class="text-end pe-3 small fw-semibold text-muted">@grp.Key subtotal</td>
<td class="text-end fw-semibold">@grp.Sum(l => l.DebitBalance).ToString("C")</td>
<td class="text-end fw-semibold">@grp.Sum(l => l.CreditBalance).ToString("C")</td>
</tr>
}
</tbody>
<tfoot class="table-dark fw-bold">
<tr>
<td colspan="2" class="text-end pe-3">Total</td>
<td class="text-end">@Model.TotalDebits.ToString("C")</td>
<td class="text-end">@Model.TotalCredits.ToString("C")</td>
</tr>
@if (!Model.IsBalanced)
{
<tr class="table-danger">
<td colspan="2" class="text-end pe-3 text-danger">Difference (out of balance)</td>
<td class="text-end text-danger" colspan="2">@((Model.TotalDebits - Model.TotalCredits).ToString("C"))</td>
</tr>
}
</tfoot>
</table>
</div>
</div>
}
<div class="text-muted small mt-2 no-print">
<i class="bi bi-info-circle me-1"></i>
Generated @DateTime.Now.ToString("MMM d, yyyy h:mm tt") · Uses current account balances (live, not point-in-time). Accounts with zero balance are excluded.
</div>