Phase F: Customer/Vendor Statements, Payment Terms Parser, Tax Rates
F1: GetCustomerStatementAsync/GetVendorStatementAsync on IFinancialReportService;
StatementLineDto; CustomerStatementDto/VendorStatementDto; Statement action on
CustomersController + VendorsController; Statement views + PDF download via
StatementPdfHelper (QuestPDF); Statement button on Customer/Vendor Details pages.
F2: PaymentTermsParser static helper (CalculateDueDate, ParseEarlyPaymentDiscount);
EarlyPaymentDiscountPercent/Days on Invoice entity; GetCustomerPaymentTerms AJAX
endpoint on InvoicesController auto-populates Terms + due date on customer select;
early payment discount notice on Invoice Create.
F3: TaxRate entity (Name/Rate/State/IsDefault/IsActive, tenant-filtered);
IUnitOfWork.TaxRates + UnitOfWork + ApplicationDbContext; TaxRatesController
(Index/Create/Edit/Delete/ToggleActive, CompanyAdminOnly); GetTaxRateForCustomer
AJAX endpoint; Tax Rates in Settings gear menu.
Also fixes AddVendorCredits migration: VendorCreditApplications FKs changed from
CASCADE to NoAction to resolve SQL Server error 1785 (multiple cascade paths).
Migration: AddPaymentTermsAndTaxRates applied locally; 200/200 unit tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -459,6 +459,9 @@
|
||||
<a asp-action="Invoices" asp-route-id="@Model.Id" class="btn btn-outline-warning">
|
||||
<i class="bi bi-receipt me-2"></i>View Invoices
|
||||
</a>
|
||||
<a asp-action="Statement" asp-route-id="@Model.Id" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-journal-text me-2"></i>Statement
|
||||
</a>
|
||||
<a asp-controller="Jobs" asp-action="Create" asp-route-customerId="@Model.Id" class="btn btn-outline-success">
|
||||
<i class="bi bi-plus-circle me-2"></i>New Job
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
@model PowderCoating.Application.DTOs.Accounting.CustomerStatementDto
|
||||
@{
|
||||
ViewData["Title"] = $"Statement – {Model.CustomerName}";
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-start mb-4 flex-wrap gap-2">
|
||||
<div>
|
||||
<h4 class="mb-0">Customer Statement</h4>
|
||||
<p class="text-muted mb-0">@Model.CustomerName · @Model.From.ToString("MMM d, yyyy") – @Model.To.ToString("MMM d, yyyy")</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<form method="get" class="d-flex gap-2 align-items-center">
|
||||
<input type="hidden" name="id" value="@(ViewContext.RouteData.Values["id"])" />
|
||||
<input type="date" name="from" class="form-control form-control-sm" value="@Model.From.ToString("yyyy-MM-dd")" />
|
||||
<input type="date" name="to" class="form-control form-control-sm" value="@Model.To.ToString("yyyy-MM-dd")" />
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary">Refresh</button>
|
||||
</form>
|
||||
<a asp-action="Statement" asp-route-id="@(ViewContext.RouteData.Values["id"])"
|
||||
asp-route-from="@Model.From.ToString("yyyy-MM-dd")"
|
||||
asp-route-to="@Model.To.ToString("yyyy-MM-dd")"
|
||||
asp-route-pdf="true"
|
||||
class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-file-earmark-pdf me-1"></i>Download PDF
|
||||
</a>
|
||||
<a asp-action="Details" asp-route-id="@(ViewContext.RouteData.Values["id"])" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>Back
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header border-0 py-3 bg-white d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span class="fw-semibold">@Model.CustomerName</span>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.CustomerAddress))
|
||||
{
|
||||
<span class="text-muted small ms-2">@Model.CustomerAddress</span>
|
||||
}
|
||||
</div>
|
||||
<div class="text-muted small">@Model.CompanyName</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th style="width:100px">Date</th>
|
||||
<th style="width:120px">Type</th>
|
||||
<th style="width:130px">Reference</th>
|
||||
<th>Description</th>
|
||||
<th class="text-end" style="width:110px">Debit</th>
|
||||
<th class="text-end" style="width:110px">Credit</th>
|
||||
<th class="text-end" style="width:120px">Balance</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Opening balance -->
|
||||
<tr class="table-light fw-semibold">
|
||||
<td class="text-muted">@Model.From.AddDays(-1).ToString("MM/dd/yy")</td>
|
||||
<td colspan="5">Opening Balance</td>
|
||||
<td class="text-end">@Model.OpeningBalance.ToString("C")</td>
|
||||
</tr>
|
||||
|
||||
@if (!Model.Lines.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="7" class="text-center text-muted py-4">No activity in this period.</td>
|
||||
</tr>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var line in Model.Lines)
|
||||
{
|
||||
<tr>
|
||||
<td class="text-muted small">@line.Date.ToString("MM/dd/yy")</td>
|
||||
<td>
|
||||
<span class="badge @(line.Type == "Invoice" ? "bg-primary" : line.Type == "Payment" ? "bg-success" : "bg-secondary") text-white">
|
||||
@line.Type
|
||||
</span>
|
||||
</td>
|
||||
<td class="small">@line.Reference</td>
|
||||
<td class="small text-muted">@line.Description</td>
|
||||
<td class="text-end small">@(line.Debit.HasValue ? line.Debit.Value.ToString("C") : "")</td>
|
||||
<td class="text-end small">@(line.Credit.HasValue ? line.Credit.Value.ToString("C") : "")</td>
|
||||
<td class="text-end small @(line.RunningBalance > 0 ? "text-danger" : "text-success") fw-semibold">
|
||||
@line.RunningBalance.ToString("C")
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Closing balance -->
|
||||
<tr class="table-secondary fw-bold">
|
||||
<td colspan="6">Closing Balance</td>
|
||||
<td class="text-end @(Model.ClosingBalance > 0 ? "text-danger" : "text-success")">
|
||||
@Model.ClosingBalance.ToString("C")
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,6 +180,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<select asp-for="Terms" asp-items="ViewBag.PaymentTermsOptions" class="form-select"></select>
|
||||
<div id="earlyPaymentDiscountNotice" class="form-text text-success d-none"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -456,11 +457,52 @@
|
||||
function onCustomerChanged(select) {
|
||||
document.getElementById('hiddenCustomerId').value = select.value;
|
||||
const customerId = parseInt(select.value) || 0;
|
||||
const taxField = document.getElementById('TaxPercent');
|
||||
if (taxField) {
|
||||
taxField.value = taxExemptCustomerIds.has(customerId) ? 0 : companyTaxPercent;
|
||||
recalcTotals();
|
||||
}
|
||||
if (!customerId) return;
|
||||
|
||||
// Fetch payment terms + tax rate for the selected customer
|
||||
fetch(`/Invoices/GetCustomerPaymentTerms?customerId=${customerId}`)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(data => {
|
||||
if (!data) return;
|
||||
// Auto-fill Terms dropdown
|
||||
const termsSelect = document.getElementById('Terms');
|
||||
if (termsSelect && data.paymentTerms) {
|
||||
// Set the matching option, or fall back to the raw value
|
||||
const opt = Array.from(termsSelect.options).find(o => o.value === data.paymentTerms);
|
||||
if (opt) termsSelect.value = data.paymentTerms;
|
||||
// Trigger due date recalculation (invoice-due-date.js listens to 'change')
|
||||
termsSelect.dispatchEvent(new Event('change'));
|
||||
}
|
||||
// Show/hide early payment discount notice
|
||||
const discountEl = document.getElementById('earlyPaymentDiscountNotice');
|
||||
if (discountEl) {
|
||||
if (data.earlyPaymentDiscountPercent > 0) {
|
||||
discountEl.textContent = `${data.earlyPaymentDiscountPercent}% discount if paid within ${data.earlyPaymentDiscountDays} days`;
|
||||
discountEl.classList.remove('d-none');
|
||||
} else {
|
||||
discountEl.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
}).catch(() => {});
|
||||
|
||||
// Fetch tax rate for the selected customer
|
||||
fetch(`/Invoices/GetTaxRateForCustomer?customerId=${customerId}`)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(data => {
|
||||
if (!data) return;
|
||||
const taxField = document.getElementById('TaxPercent');
|
||||
if (taxField) {
|
||||
taxField.value = data.taxPercent ?? 0;
|
||||
recalcTotals();
|
||||
}
|
||||
}).catch(() => {
|
||||
// Fall back to client-side tax exempt check
|
||||
const taxField = document.getElementById('TaxPercent');
|
||||
if (taxField) {
|
||||
taxField.value = taxExemptCustomerIds.has(customerId) ? 0 : companyTaxPercent;
|
||||
recalcTotals();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Merchandise combobox ────────────────────────────────────────────────
|
||||
|
||||
@@ -1516,6 +1516,7 @@
|
||||
{
|
||||
<li><a class="dropdown-item" asp-controller="CompanyUsers" asp-action="Index"><i class="bi bi-people-fill me-2"></i>Manage Users</a></li>
|
||||
<li><a class="dropdown-item" asp-controller="PricingTiers" asp-action="Index"><i class="bi bi-tags me-2"></i>Pricing Tiers</a></li>
|
||||
<li><a class="dropdown-item" asp-controller="TaxRates" asp-action="Index"><i class="bi bi-percent me-2"></i>Tax Rates</a></li>
|
||||
}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
@if (gearIsAdmin)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
@model PowderCoating.Core.Entities.TaxRate
|
||||
@{
|
||||
ViewData["Title"] = "Add Tax Rate";
|
||||
}
|
||||
|
||||
<div class="mb-4">
|
||||
<h4 class="mb-0">Add Tax Rate</h4>
|
||||
<p class="text-muted small mb-0">Define a named tax rate to auto-fill invoice tax percent by jurisdiction.</p>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm" style="max-width:600px">
|
||||
<div class="card-body">
|
||||
<form asp-action="Create" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Name" class="form-label fw-semibold">Name <span class="text-danger">*</span></label>
|
||||
<input asp-for="Name" class="form-control" placeholder="e.g., CA Sales Tax" />
|
||||
<span asp-validation-for="Name" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Rate" class="form-label fw-semibold">Rate (%) <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<input asp-for="Rate" type="number" step="0.0001" min="0" max="100" class="form-control" placeholder="8.25" />
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
<span asp-validation-for="Rate" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="State" class="form-label fw-semibold">State</label>
|
||||
<input asp-for="State" class="form-control" placeholder="e.g., CA" maxlength="2" />
|
||||
<span asp-validation-for="State" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Description" class="form-label fw-semibold">Description</label>
|
||||
<input asp-for="Description" class="form-control" placeholder="Optional notes" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input asp-for="IsDefault" class="form-check-input" type="checkbox" />
|
||||
<label asp-for="IsDefault" class="form-check-label">
|
||||
Default rate
|
||||
<span class="text-muted small d-block">Automatically applied to new invoices for taxable customers.</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 form-check">
|
||||
<input asp-for="IsActive" class="form-check-input" type="checkbox" checked />
|
||||
<label asp-for="IsActive" class="form-check-label">Active</label>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">Save Tax Rate</button>
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,62 @@
|
||||
@model PowderCoating.Core.Entities.TaxRate
|
||||
@{
|
||||
ViewData["Title"] = "Edit Tax Rate";
|
||||
}
|
||||
|
||||
<div class="mb-4">
|
||||
<h4 class="mb-0">Edit Tax Rate</h4>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm" style="max-width:600px">
|
||||
<div class="card-body">
|
||||
<form asp-action="Edit" asp-route-id="@Model.Id" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" asp-for="Id" />
|
||||
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Name" class="form-label fw-semibold">Name <span class="text-danger">*</span></label>
|
||||
<input asp-for="Name" class="form-control" />
|
||||
<span asp-validation-for="Name" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Rate" class="form-label fw-semibold">Rate (%) <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<input asp-for="Rate" type="number" step="0.0001" min="0" max="100" class="form-control" />
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
<span asp-validation-for="Rate" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="State" class="form-label fw-semibold">State</label>
|
||||
<input asp-for="State" class="form-control" maxlength="2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Description" class="form-label fw-semibold">Description</label>
|
||||
<input asp-for="Description" class="form-control" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input asp-for="IsDefault" class="form-check-input" type="checkbox" />
|
||||
<label asp-for="IsDefault" class="form-check-label">
|
||||
Default rate
|
||||
<span class="text-muted small d-block">Automatically applied to new invoices for taxable customers.</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 form-check">
|
||||
<input asp-for="IsActive" class="form-check-input" type="checkbox" />
|
||||
<label asp-for="IsActive" class="form-check-label">Active</label>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,108 @@
|
||||
@model List<PowderCoating.Core.Entities.TaxRate>
|
||||
@{
|
||||
ViewData["Title"] = "Tax Rates";
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h4 class="mb-0">Tax Rates</h4>
|
||||
<p class="text-muted mb-0 small">Named rates used to auto-fill invoice tax percent when a taxable customer is selected.</p>
|
||||
</div>
|
||||
<a asp-action="Create" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-circle me-1"></i>Add Tax Rate
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if (TempData["Success"] != null)
|
||||
{
|
||||
<div class="alert alert-success alert-permanent alert-dismissible fade show" role="alert">
|
||||
@TempData["Success"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
}
|
||||
@if (TempData["Error"] != null)
|
||||
{
|
||||
<div class="alert alert-danger alert-permanent alert-dismissible fade show" role="alert">
|
||||
@TempData["Error"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-percent fs-1 d-block mb-3 opacity-25"></i>
|
||||
<p class="mb-1">No tax rates defined yet.</p>
|
||||
<p class="small">Add a rate and mark it as default to auto-populate tax on invoices.</p>
|
||||
<a asp-action="Create" class="btn btn-primary btn-sm mt-2">
|
||||
<i class="bi bi-plus-circle me-1"></i>Add First Tax Rate
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Rate</th>
|
||||
<th>State</th>
|
||||
<th>Description</th>
|
||||
<th class="text-center">Default</th>
|
||||
<th class="text-center">Active</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var rate in Model)
|
||||
{
|
||||
<tr class="@(!rate.IsActive ? "opacity-50" : "")">
|
||||
<td>
|
||||
@rate.Name
|
||||
@if (rate.IsDefault)
|
||||
{
|
||||
<span class="badge bg-success ms-1">Default</span>
|
||||
}
|
||||
</td>
|
||||
<td>@rate.Rate.ToString("0.##")%</td>
|
||||
<td>@(rate.State ?? "—")</td>
|
||||
<td class="text-muted small">@(rate.Description ?? "—")</td>
|
||||
<td class="text-center">
|
||||
@if (rate.IsDefault)
|
||||
{
|
||||
<i class="bi bi-check-circle-fill text-success"></i>
|
||||
}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<form asp-action="ToggleActive" asp-route-id="@rate.Id" method="post" class="d-inline">
|
||||
@Html.AntiForgeryToken()
|
||||
<button type="submit" class="btn btn-sm btn-link p-0 border-0"
|
||||
title="@(rate.IsActive ? "Deactivate" : "Activate")">
|
||||
<i class="bi @(rate.IsActive ? "bi-toggle-on text-success fs-5" : "bi-toggle-off text-muted fs-5")"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a asp-action="Edit" asp-route-id="@rate.Id" class="btn btn-sm btn-outline-secondary me-1">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
@if (!rate.IsDefault)
|
||||
{
|
||||
<form asp-action="Delete" asp-route-id="@rate.Id" method="post" class="d-inline"
|
||||
onsubmit="return confirm('Delete this tax rate?')">
|
||||
@Html.AntiForgeryToken()
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -10,6 +10,9 @@
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10">
|
||||
<div class="d-flex justify-content-end gap-2 mb-4">
|
||||
<a asp-action="Statement" asp-route-id="@Model.Id" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-journal-text me-2"></i>Statement
|
||||
</a>
|
||||
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-warning">
|
||||
<i class="bi bi-pencil me-2"></i>Edit
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
@model PowderCoating.Application.DTOs.Accounting.VendorStatementDto
|
||||
@{
|
||||
ViewData["Title"] = $"Statement – {Model.VendorName}";
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-start mb-4 flex-wrap gap-2">
|
||||
<div>
|
||||
<h4 class="mb-0">Vendor Statement</h4>
|
||||
<p class="text-muted mb-0">@Model.VendorName · @Model.From.ToString("MMM d, yyyy") – @Model.To.ToString("MMM d, yyyy")</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<form method="get" class="d-flex gap-2 align-items-center">
|
||||
<input type="hidden" name="id" value="@(ViewContext.RouteData.Values["id"])" />
|
||||
<input type="date" name="from" class="form-control form-control-sm" value="@Model.From.ToString("yyyy-MM-dd")" />
|
||||
<input type="date" name="to" class="form-control form-control-sm" value="@Model.To.ToString("yyyy-MM-dd")" />
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary">Refresh</button>
|
||||
</form>
|
||||
<a asp-action="Statement" asp-route-id="@(ViewContext.RouteData.Values["id"])"
|
||||
asp-route-from="@Model.From.ToString("yyyy-MM-dd")"
|
||||
asp-route-to="@Model.To.ToString("yyyy-MM-dd")"
|
||||
asp-route-pdf="true"
|
||||
class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-file-earmark-pdf me-1"></i>Download PDF
|
||||
</a>
|
||||
<a asp-action="Details" asp-route-id="@(ViewContext.RouteData.Values["id"])" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>Back
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header border-0 py-3 bg-white d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">@Model.VendorName</span>
|
||||
<div class="text-muted small">@Model.CompanyName</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th style="width:100px">Date</th>
|
||||
<th style="width:120px">Type</th>
|
||||
<th style="width:130px">Reference</th>
|
||||
<th>Description</th>
|
||||
<th class="text-end" style="width:110px">Debit</th>
|
||||
<th class="text-end" style="width:110px">Credit</th>
|
||||
<th class="text-end" style="width:120px">Balance</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="table-light fw-semibold">
|
||||
<td class="text-muted">@Model.From.AddDays(-1).ToString("MM/dd/yy")</td>
|
||||
<td colspan="5">Opening Balance</td>
|
||||
<td class="text-end">@Model.OpeningBalance.ToString("C")</td>
|
||||
</tr>
|
||||
|
||||
@if (!Model.Lines.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="7" class="text-center text-muted py-4">No activity in this period.</td>
|
||||
</tr>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var line in Model.Lines)
|
||||
{
|
||||
<tr>
|
||||
<td class="text-muted small">@line.Date.ToString("MM/dd/yy")</td>
|
||||
<td>
|
||||
<span class="badge @(line.Type == "Bill" ? "bg-danger" : line.Type == "Payment" ? "bg-success" : "bg-secondary") text-white">
|
||||
@line.Type
|
||||
</span>
|
||||
</td>
|
||||
<td class="small">@line.Reference</td>
|
||||
<td class="small text-muted">@line.Description</td>
|
||||
<td class="text-end small">@(line.Debit.HasValue ? line.Debit.Value.ToString("C") : "")</td>
|
||||
<td class="text-end small">@(line.Credit.HasValue ? line.Credit.Value.ToString("C") : "")</td>
|
||||
<td class="text-end small @(line.RunningBalance > 0 ? "text-danger" : "text-success") fw-semibold">
|
||||
@line.RunningBalance.ToString("C")
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
|
||||
<tr class="table-secondary fw-bold">
|
||||
<td colspan="6">Closing Balance</td>
|
||||
<td class="text-end @(Model.ClosingBalance > 0 ? "text-danger" : "text-success")">
|
||||
@Model.ClosingBalance.ToString("C")
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user