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:
@@ -426,6 +426,98 @@ public class FinancialReportService : IFinancialReportService
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<SalesTaxReportDto> GetSalesTaxReportAsync(int companyId, DateTime from, DateTime to)
|
||||
{
|
||||
var toEnd = to.AddDays(1).AddTicks(-1);
|
||||
var companyName = await GetCompanyNameAsync(companyId);
|
||||
|
||||
var invoices = await _context.Invoices
|
||||
.Include(i => i.Customer)
|
||||
.Include(i => i.SalesTaxAccount)
|
||||
.Where(i => i.CompanyId == companyId
|
||||
&& i.Status != InvoiceStatus.Draft
|
||||
&& i.Status != InvoiceStatus.Voided
|
||||
&& i.InvoiceDate >= from && i.InvoiceDate <= toEnd)
|
||||
.AsNoTracking()
|
||||
.OrderBy(i => i.InvoiceDate)
|
||||
.ToListAsync();
|
||||
|
||||
var taxable = invoices.Where(i => i.TaxAmount > 0).ToList();
|
||||
var nonTaxable = invoices.Where(i => i.TaxAmount == 0).ToList();
|
||||
|
||||
var byAccount = invoices
|
||||
.Where(i => i.TaxAmount > 0)
|
||||
.GroupBy(i => new
|
||||
{
|
||||
i.SalesTaxAccountId,
|
||||
AccountName = i.SalesTaxAccount?.Name ?? "Unassigned",
|
||||
AccountNumber = i.SalesTaxAccount?.AccountNumber ?? string.Empty
|
||||
})
|
||||
.Select(g => new SalesTaxByAccountDto
|
||||
{
|
||||
AccountId = g.Key.SalesTaxAccountId,
|
||||
AccountName = g.Key.AccountName,
|
||||
AccountNumber = g.Key.AccountNumber,
|
||||
TaxableSales = g.Sum(i => i.SubTotal),
|
||||
TaxBilled = g.Sum(i => i.TaxAmount),
|
||||
InvoiceCount = g.Count()
|
||||
})
|
||||
.OrderBy(a => a.AccountNumber)
|
||||
.ThenBy(a => a.AccountName)
|
||||
.ToList();
|
||||
|
||||
var byMonth = invoices
|
||||
.Where(i => i.TaxAmount > 0)
|
||||
.GroupBy(i => new { i.InvoiceDate.Year, i.InvoiceDate.Month })
|
||||
.Select(g => new SalesTaxByMonthDto
|
||||
{
|
||||
Year = g.Key.Year,
|
||||
Month = g.Key.Month,
|
||||
Label = new DateTime(g.Key.Year, g.Key.Month, 1).ToString("MMM yyyy"),
|
||||
TaxableSales = g.Sum(i => i.SubTotal),
|
||||
TaxBilled = g.Sum(i => i.TaxAmount),
|
||||
InvoiceCount = g.Count()
|
||||
})
|
||||
.OrderBy(m => m.Year).ThenBy(m => m.Month)
|
||||
.ToList();
|
||||
|
||||
var invoiceLines = invoices.Select(i => new SalesTaxInvoiceLineDto
|
||||
{
|
||||
InvoiceId = i.Id,
|
||||
InvoiceNumber = i.InvoiceNumber,
|
||||
CustomerName = i.Customer!.IsCommercial
|
||||
? i.Customer.CompanyName ?? string.Empty
|
||||
: $"{i.Customer.ContactFirstName} {i.Customer.ContactLastName}".Trim(),
|
||||
InvoiceDate = i.InvoiceDate,
|
||||
Status = i.Status.ToString(),
|
||||
SubTotal = i.SubTotal,
|
||||
TaxPercent = i.TaxPercent,
|
||||
TaxAmount = i.TaxAmount,
|
||||
Total = i.Total,
|
||||
AmountPaid = i.AmountPaid,
|
||||
BalanceDue = i.BalanceDue,
|
||||
TaxAccountName = i.SalesTaxAccount != null
|
||||
? $"{i.SalesTaxAccount.AccountNumber} {i.SalesTaxAccount.Name}".Trim()
|
||||
: string.Empty
|
||||
}).ToList();
|
||||
|
||||
return new SalesTaxReportDto
|
||||
{
|
||||
From = from,
|
||||
To = to,
|
||||
CompanyName = companyName,
|
||||
TotalTaxableSales = taxable.Sum(i => i.SubTotal),
|
||||
TotalNonTaxableSales = nonTaxable.Sum(i => i.SubTotal),
|
||||
TotalTaxBilled = taxable.Sum(i => i.TaxAmount),
|
||||
TaxableInvoiceCount = taxable.Count,
|
||||
NonTaxableInvoiceCount = nonTaxable.Count,
|
||||
ByAccount = byAccount,
|
||||
ByMonth = byMonth,
|
||||
Invoices = invoiceLines
|
||||
};
|
||||
}
|
||||
|
||||
/// <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