Add Sales Tax Liability report with PDF and CSV export

Invoice-basis report showing taxable vs non-taxable sales, tax billed
by GL account, monthly trend table/chart, and full invoice detail grid.
Non-taxable invoice rows shaded grey for easy scanning. Quick-preset
date buttons (This Month, Last Month, YTD, Last Year) for common filing
periods. CSV export formatted for accountants and tax-filing software.
Gated behind AllowAccounting() like other financial reports.

- SalesTaxReportDto + 3 supporting DTOs in FinancialReportDtos.cs
- GetSalesTaxReportAsync on IFinancialReportService + implementation
- GenerateSalesTaxReportPdfAsync on IPdfService + QuestPDF implementation
- SalesTax / SalesTaxPdf / SalesTaxCsv actions in ReportsController
- Views/Reports/SalesTax.cshtml with Chart.js monthly trend chart
- Landing page card added to Finance section
- HelpKnowledgeBase and Help/Reports.cshtml updated with full docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 12:27:08 -04:00
parent 7e0699d5bd
commit ca4fb959aa
11 changed files with 898 additions and 7 deletions
@@ -1124,6 +1124,74 @@ public class ReportsController : Controller
return inline ? File(pdfBytes, "application/pdf") : File(pdfBytes, "application/pdf", $"SalesAndIncome-{fromDate:yyyyMMdd}-{toDate:yyyyMMdd}.pdf");
}
/// <summary>
/// Sales Tax Liability report (invoice basis). Shows taxable vs non-taxable sales,
/// total tax billed, breakdown by tax account and by month, and a full invoice detail grid.
/// Gated behind <see cref="AllowAccounting"/>.
/// </summary>
// GET: /Reports/SalesTax
public async Task<IActionResult> SalesTax(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.GetSalesTaxReportAsync(companyId, fromDate, toDate);
return View(dto);
}
/// <summary>
/// PDF export of the Sales Tax Liability report. Same inline/attachment pattern as other PDF actions.
/// Gated behind <see cref="AllowAccounting"/>.
/// </summary>
// GET: /Reports/SalesTaxPdf
public async Task<IActionResult> SalesTaxPdf(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.GetSalesTaxReportAsync(companyId, fromDate, toDate);
var pdfBytes = await _pdfService.GenerateSalesTaxReportPdfAsync(dto);
return inline ? File(pdfBytes, "application/pdf") : File(pdfBytes, "application/pdf", $"SalesTaxReport-{fromDate:yyyyMMdd}-{toDate:yyyyMMdd}.pdf");
}
/// <summary>
/// CSV export of the Sales Tax Liability report. Returns one row per invoice, suitable
/// for handing to an accountant or importing into tax filing software.
/// Gated behind <see cref="AllowAccounting"/>.
/// </summary>
// GET: /Reports/SalesTaxCsv
public async Task<IActionResult> SalesTaxCsv(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.GetSalesTaxReportAsync(companyId, fromDate, toDate);
var sb = new System.Text.StringBuilder();
sb.AppendLine("Invoice #,Customer,Date,Status,Subtotal,Tax %,Tax Amount,Total,Amount Paid,Balance Due,Tax Account");
foreach (var inv in dto.Invoices)
{
sb.AppendLine(string.Join(",",
$"\"{inv.InvoiceNumber}\"",
$"\"{inv.CustomerName.Replace("\"", "\"\"")}\"",
inv.InvoiceDate.ToString("yyyy-MM-dd"),
$"\"{inv.Status}\"",
inv.SubTotal.ToString("F2"),
inv.TaxPercent.ToString("F4"),
inv.TaxAmount.ToString("F2"),
inv.Total.ToString("F2"),
inv.AmountPaid.ToString("F2"),
inv.BalanceDue.ToString("F2"),
$"\"{inv.TaxAccountName.Replace("\"", "\"\"")}\""));
}
var bytes = System.Text.Encoding.UTF8.GetPreamble().Concat(System.Text.Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
return File(bytes, "text/csv", $"SalesTaxReport-{fromDate:yyyyMMdd}-{toDate:yyyyMMdd}.csv");
}
// ── INDIVIDUAL REPORT PAGES ──────────────────────────────────────────────
/// <summary>