a0bdd2b5b4
Replace all corruption variants with HTML entities across 226 view files: - 3-char UTF-8-as-Win1252 sequences (ae-corruption) - Standalone smart/curly quotes that break C# Razor expressions - Partially re-corrupted variants where the 3rd byte was normalised to ASCII tools/Fix-Encoding.ps1: re-runnable sweep; uses [char] code points so the script itself never contains a literal non-ASCII character; supports -DryRun .githooks/pre-commit: blocks commits containing the ae-corruption byte signature (xc3xa2xe2x82xac); git core.hooksPath = .githooks so the hook is repo-committed and active for all future work on this machine. Build clean; 225 unit tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
467 lines
25 KiB
Plaintext
467 lines
25 KiB
Plaintext
@model PowderCoating.Application.DTOs.Accounting.BillDto
|
|
@using PowderCoating.Core.Enums
|
|
|
|
@{
|
|
ViewData["Title"] = $"Bill {Model.BillNumber}";
|
|
ViewData["PageIcon"] = "bi-receipt-cutoff";
|
|
ViewData["PageHelpTitle"] = "Bill Status";
|
|
ViewData["PageHelpContent"] = "Draft: editable, not yet confirmed. Open: awaiting payment. Partially Paid: some payments recorded. Paid: fully settled. Voided: cancelled — preserves history. Edit is only available in Draft status. Use Void instead of deleting to keep a complete audit trail.";
|
|
|
|
string StatusBadge(BillStatus s) => s switch
|
|
{
|
|
BillStatus.Draft => "secondary",
|
|
BillStatus.Open => "primary",
|
|
BillStatus.PartiallyPaid => "warning",
|
|
BillStatus.Paid => "success",
|
|
BillStatus.Voided => "danger",
|
|
_ => "secondary"
|
|
};
|
|
}
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<a asp-action="Index" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left"></i></a>
|
|
<span class="badge bg-@StatusBadge(Model.Status) fs-6">@Model.Status</span>
|
|
<span class="text-muted small">@Model.VendorName</span>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
@if (Model.Status == BillStatus.Draft || Model.Status == BillStatus.Open || Model.Status == BillStatus.PartiallyPaid)
|
|
{
|
|
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-outline-secondary">
|
|
<i class="bi bi-pencil me-1"></i>Edit
|
|
</a>
|
|
}
|
|
@if (Model.Status == BillStatus.Open || Model.Status == BillStatus.PartiallyPaid)
|
|
{
|
|
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#paymentModal">
|
|
<i class="bi bi-cash me-1"></i>Record Payment
|
|
</button>
|
|
}
|
|
@if (Model.Status != BillStatus.Voided && Model.Status != BillStatus.Paid)
|
|
{
|
|
<form asp-action="Void" asp-route-id="@Model.Id" method="post"
|
|
onsubmit="return confirm('Void bill @Model.BillNumber? This cannot be undone.')">
|
|
@Html.AntiForgeryToken()
|
|
<button type="submit" class="btn btn-outline-danger">
|
|
<i class="bi bi-x-circle me-1"></i>Void
|
|
</button>
|
|
</form>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@if (TempData["Success"] != null)
|
|
{
|
|
<div class="alert alert-success alert-dismissible fade show">
|
|
<i class="bi bi-check-circle me-2"></i>@TempData["Success"]
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
}
|
|
@if (TempData["Error"] != null)
|
|
{
|
|
<div class="alert alert-danger alert-dismissible fade show">
|
|
<i class="bi bi-exclamation-triangle me-2"></i>@TempData["Error"]
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
}
|
|
|
|
<div class="row g-4">
|
|
<!-- Left column -->
|
|
<div class="col-lg-8">
|
|
|
|
<!-- Bill info -->
|
|
<div class="card shadow-sm mb-4">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-sm-6">
|
|
<p class="text-muted small mb-1">Vendor</p>
|
|
<p class="fw-medium mb-0">@Model.VendorName</p>
|
|
@if (!string.IsNullOrEmpty(Model.VendorEmail))
|
|
{ <p class="text-muted small mb-0">@Model.VendorEmail</p> }
|
|
@if (!string.IsNullOrEmpty(Model.VendorPhone))
|
|
{ <p class="text-muted small mb-0">@Model.VendorPhone</p> }
|
|
</div>
|
|
<div class="col-sm-6">
|
|
<div class="row g-2">
|
|
<div class="col-6">
|
|
<p class="text-muted small mb-1">Bill Date</p>
|
|
<p class="mb-0">@Model.BillDate.ToString("MMM d, yyyy")</p>
|
|
</div>
|
|
@if (Model.DueDate.HasValue)
|
|
{
|
|
<div class="col-6">
|
|
<p class="text-muted small mb-1">Due Date</p>
|
|
<p class="mb-0 @(Model.Status != BillStatus.Paid && Model.DueDate < DateTime.Today ? "text-danger fw-medium" : "")">
|
|
@Model.DueDate.Value.ToString("MMM d, yyyy")
|
|
</p>
|
|
</div>
|
|
}
|
|
@if (!string.IsNullOrEmpty(Model.VendorInvoiceNumber))
|
|
{
|
|
<div class="col-6">
|
|
<p class="text-muted small mb-1">Vendor Ref #</p>
|
|
<p class="mb-0">@Model.VendorInvoiceNumber</p>
|
|
</div>
|
|
}
|
|
@if (!string.IsNullOrEmpty(Model.Terms))
|
|
{
|
|
<div class="col-6">
|
|
<p class="text-muted small mb-1">Terms</p>
|
|
<p class="mb-0">@Model.Terms</p>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
@if (!string.IsNullOrEmpty(Model.Memo))
|
|
{
|
|
<div class="col-12">
|
|
<p class="text-muted small mb-1">Memo</p>
|
|
<p class="mb-0">@Model.Memo</p>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Line items -->
|
|
<div class="card shadow-sm mb-4">
|
|
<div class="card-header fw-semibold">Line Items</div>
|
|
<div class="card-body p-0">
|
|
<table class="table table-sm mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Account</th>
|
|
<th>Description</th>
|
|
<th>Job</th>
|
|
<th class="text-center">Qty</th>
|
|
<th class="text-end">Unit Price</th>
|
|
<th class="text-end">Amount</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var li in Model.LineItems.OrderBy(l => l.DisplayOrder))
|
|
{
|
|
<tr>
|
|
<td><span class="text-muted small">@li.AccountNumber</span> @li.AccountName</td>
|
|
<td>@li.Description</td>
|
|
<td>@li.JobNumber</td>
|
|
<td class="text-center">@li.Quantity.ToString("G")</td>
|
|
<td class="text-end">@li.UnitPrice.ToString("C")</td>
|
|
<td class="text-end fw-medium">@li.Amount.ToString("C")</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
<tfoot class="table-light">
|
|
<tr>
|
|
<td colspan="5" class="text-end text-muted">Subtotal</td>
|
|
<td class="text-end">@Model.SubTotal.ToString("C")</td>
|
|
</tr>
|
|
@if (Model.TaxPercent > 0)
|
|
{
|
|
<tr>
|
|
<td colspan="5" class="text-end text-muted">Tax (@Model.TaxPercent.ToString("G")%)</td>
|
|
<td class="text-end">@Model.TaxAmount.ToString("C")</td>
|
|
</tr>
|
|
}
|
|
<tr class="fw-bold">
|
|
<td colspan="5" class="text-end">Total</td>
|
|
<td class="text-end fs-5">@Model.Total.ToString("C")</td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Receipt attachment -->
|
|
@if (!string.IsNullOrEmpty(Model.ReceiptFilePath))
|
|
{
|
|
<div class="card shadow-sm mb-4">
|
|
<div class="card-header fw-semibold"><i class="bi bi-paperclip me-2"></i>Receipt / Document</div>
|
|
<div class="card-body d-flex align-items-center gap-3">
|
|
<a asp-action="DownloadReceipt" asp-route-id="@Model.Id"
|
|
class="btn btn-outline-secondary" target="_blank">
|
|
<i class="bi bi-download me-1"></i>Download Attachment
|
|
</a>
|
|
@if (Model.Status == PowderCoating.Core.Enums.BillStatus.Draft)
|
|
{
|
|
<form asp-action="RemoveReceipt" asp-route-id="@Model.Id" method="post" class="d-inline">
|
|
@Html.AntiForgeryToken()
|
|
<button type="submit" class="btn btn-outline-danger btn-sm"
|
|
onclick="return confirm('Remove this receipt attachment?')">
|
|
<i class="bi bi-trash me-1"></i>Remove
|
|
</button>
|
|
</form>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<!-- Payment history -->
|
|
@if (Model.Payments.Any())
|
|
{
|
|
<div class="card shadow-sm">
|
|
<div class="card-header fw-semibold d-flex align-items-center gap-2">
|
|
Payment History
|
|
<a tabindex="0" class="help-icon" role="button"
|
|
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
|
data-bs-title="Payment History"
|
|
data-bs-content="All payments applied to this bill. Each records the date, method, bank account, and optional check number for reconciliation. Delete a payment to reverse it and restore the balance due. Check # is useful for matching against your bank statement.">
|
|
<i class="bi bi-question-circle"></i>
|
|
</a>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<table class="table table-sm mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Payment #</th>
|
|
<th>Date</th>
|
|
<th>Method</th>
|
|
<th>Check #</th>
|
|
<th>Bank Account</th>
|
|
<th>Memo</th>
|
|
<th class="text-end">Amount</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var pmt in Model.Payments.OrderByDescending(p => p.PaymentDate))
|
|
{
|
|
<tr>
|
|
<td><span class="text-muted small">@pmt.PaymentNumber</span></td>
|
|
<td>@pmt.PaymentDate.ToString("MMM d, yyyy")</td>
|
|
<td>@pmt.PaymentMethod</td>
|
|
<td>@pmt.CheckNumber</td>
|
|
<td>@pmt.BankAccountName</td>
|
|
<td><span class="text-muted small">@pmt.Memo</span></td>
|
|
<td class="text-end text-success fw-medium">@pmt.Amount.ToString("C")</td>
|
|
<td class="text-end">
|
|
<button type="button" class="btn btn-sm btn-outline-secondary me-1" title="Edit payment"
|
|
onclick="openEditBillPaymentModal(@pmt.Id, @Model.Id, '@pmt.PaymentDate.ToString("yyyy-MM-dd")', @((int)pmt.PaymentMethod), @pmt.BankAccountId, '@(pmt.CheckNumber ?? "")', '@(pmt.Memo ?? "")')">
|
|
<i class="bi bi-pencil"></i>
|
|
</button>
|
|
<form asp-action="DeletePayment" method="post" class="d-inline"
|
|
onsubmit="return confirm('Delete this payment? The bill balance will be restored.')">
|
|
@Html.AntiForgeryToken()
|
|
<input type="hidden" name="paymentId" value="@pmt.Id" />
|
|
<input type="hidden" name="billId" value="@Model.Id" />
|
|
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete payment">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Right column: Balance summary -->
|
|
<div class="col-lg-4">
|
|
<div class="card shadow-sm border-@(Model.Status == BillStatus.Paid ? "success" : Model.BalanceDue > 0 ? "danger" : "secondary")">
|
|
<div class="card-header fw-semibold d-flex align-items-center gap-2">
|
|
Balance Summary
|
|
<a tabindex="0" class="help-icon" role="button"
|
|
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
|
|
data-bs-title="Balance Summary"
|
|
data-bs-content="Balance Due = Bill Total minus all payments recorded. Multiple partial payments are supported — each reduces the balance until fully paid. Deleting a payment reverses it and restores the balance due.">
|
|
<i class="bi bi-question-circle"></i>
|
|
</a>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between mb-2">
|
|
<span class="text-muted">Bill Total</span>
|
|
<span class="fw-medium">@Model.Total.ToString("C")</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-2">
|
|
<span class="text-muted">Amount Paid</span>
|
|
<span class="text-success fw-medium">@Model.AmountPaid.ToString("C")</span>
|
|
</div>
|
|
<hr />
|
|
<div class="d-flex justify-content-between fs-5 fw-bold">
|
|
<span>Balance Due</span>
|
|
<span class="@(Model.BalanceDue > 0 ? "text-danger" : "text-success")">
|
|
@Model.BalanceDue.ToString("C")
|
|
</span>
|
|
</div>
|
|
|
|
@if (Model.Status == BillStatus.Open || Model.Status == BillStatus.PartiallyPaid)
|
|
{
|
|
<div class="d-grid mt-3">
|
|
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#paymentModal">
|
|
<i class="bi bi-cash me-1"></i>Record Payment
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card shadow-sm mt-3">
|
|
<div class="card-header fw-semibold d-flex align-items-center gap-2">
|
|
AP Account
|
|
<a tabindex="0" class="help-icon" role="button"
|
|
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
|
|
data-bs-title="AP Account"
|
|
data-bs-content="The Accounts Payable liability account this bill is posted to. In double-entry bookkeeping, creating a bill debits the expense accounts on the line items and credits this AP account, recording the liability owed to the vendor.">
|
|
<i class="bi bi-question-circle"></i>
|
|
</a>
|
|
</div>
|
|
<div class="card-body">
|
|
<p class="mb-0 text-muted small">@Model.APAccountName</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Record Payment Modal -->
|
|
@if (Model.Status == BillStatus.Open || Model.Status == BillStatus.PartiallyPaid)
|
|
{
|
|
<div class="modal fade" id="paymentModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Record Payment — @Model.BillNumber</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form asp-action="RecordPayment" method="post">
|
|
@Html.AntiForgeryToken()
|
|
<input type="hidden" name="BillId" value="@Model.Id" />
|
|
<div class="modal-body">
|
|
<div class="row g-3">
|
|
<div class="col-sm-6">
|
|
<label class="form-label fw-medium">Payment Date <span class="text-danger">*</span></label>
|
|
<input type="date" name="PaymentDate" value="@DateTime.Today.ToString("yyyy-MM-dd")" class="form-control" required />
|
|
</div>
|
|
<div class="col-sm-6">
|
|
<label class="form-label fw-medium">Amount <span class="text-danger">*</span></label>
|
|
<div class="input-group">
|
|
<span class="input-group-text">$</span>
|
|
<input type="number" step="0.01" min="0.01" max="@Model.BalanceDue"
|
|
name="Amount" value="@Model.BalanceDue" class="form-control" required />
|
|
</div>
|
|
<div class="form-text">Balance due: @Model.BalanceDue.ToString("C")</div>
|
|
</div>
|
|
<div class="col-sm-6">
|
|
<label class="form-label fw-medium">Payment Method <span class="text-danger">*</span></label>
|
|
<select name="PaymentMethod" class="form-select" required>
|
|
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.PaymentMethods)
|
|
{
|
|
<option value="@item.Value">@item.Text</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
<div class="col-sm-6">
|
|
<div class="d-flex align-items-center gap-1 mb-1">
|
|
<label class="form-label fw-medium mb-0">Bank Account <span class="text-danger">*</span></label>
|
|
<a tabindex="0" class="help-icon" role="button"
|
|
data-bs-toggle="popover" data-bs-placement="left" data-bs-trigger="focus"
|
|
data-bs-title="Bank Account"
|
|
data-bs-content="The bank or cash account this payment is drawn from. Used for bank reconciliation — helps match this payment to the corresponding debit on your bank statement.">
|
|
<i class="bi bi-question-circle"></i>
|
|
</a>
|
|
</div>
|
|
<select name="BankAccountId" class="form-select" required>
|
|
<option value="">— Select Account —</option>
|
|
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.BankAccounts)
|
|
{
|
|
<option value="@item.Value">@item.Text</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
<div class="col-sm-6">
|
|
<label class="form-label fw-medium">Check #</label>
|
|
<input type="text" name="CheckNumber" class="form-control" placeholder="Optional" />
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label fw-medium">Memo</label>
|
|
<input type="text" name="Memo" class="form-control" placeholder="Optional notes" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-success">
|
|
<i class="bi bi-cash me-1"></i>Record Payment
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<!-- Edit Payment Modal -->
|
|
<div class="modal fade" id="editBillPaymentModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="bi bi-pencil me-2"></i>Edit Payment</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form asp-action="EditPayment" method="post">
|
|
@Html.AntiForgeryToken()
|
|
<input type="hidden" name="PaymentId" id="editBillPaymentId" />
|
|
<input type="hidden" name="BillId" value="@Model.Id" />
|
|
<div class="modal-body">
|
|
<div class="row g-3">
|
|
<div class="col-sm-6">
|
|
<label class="form-label fw-medium">Payment Date <span class="text-danger">*</span></label>
|
|
<input type="date" name="PaymentDate" id="editBillPaymentDate" class="form-control" required />
|
|
</div>
|
|
<div class="col-sm-6">
|
|
<label class="form-label fw-medium">Method <span class="text-danger">*</span></label>
|
|
<select name="PaymentMethod" id="editBillPaymentMethod" class="form-select" required>
|
|
<option value="0">Cash</option>
|
|
<option value="1">Check</option>
|
|
<option value="2">Credit/Debit Card</option>
|
|
<option value="3">Bank Transfer / ACH</option>
|
|
<option value="4">Digital Payment</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label fw-medium">Bank Account <span class="text-danger">*</span></label>
|
|
<select name="BankAccountId" id="editBillBankAccountId" class="form-select" required>
|
|
<option value="">— Select Account —</option>
|
|
@foreach (var item in (IEnumerable<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)ViewBag.BankAccounts)
|
|
{
|
|
<option value="@item.Value">@item.Text</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
<div class="col-sm-6">
|
|
<label class="form-label fw-medium">Check #</label>
|
|
<input type="text" name="CheckNumber" id="editBillCheckNumber" class="form-control" placeholder="Optional" />
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label fw-medium">Memo</label>
|
|
<input type="text" name="Memo" id="editBillMemo" class="form-control" placeholder="Optional" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="bi bi-check-circle me-1"></i>Save Changes
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@section Scripts {
|
|
<script>
|
|
function openEditBillPaymentModal(paymentId, billId, paymentDate, paymentMethod, bankAccountId, checkNumber, memo) {
|
|
document.getElementById('editBillPaymentId').value = paymentId;
|
|
document.getElementById('editBillPaymentDate').value = paymentDate;
|
|
document.getElementById('editBillPaymentMethod').value = paymentMethod;
|
|
document.getElementById('editBillBankAccountId').value = String(bankAccountId);
|
|
document.getElementById('editBillCheckNumber').value = checkNumber;
|
|
document.getElementById('editBillMemo').value = memo;
|
|
new bootstrap.Modal(document.getElementById('editBillPaymentModal')).show();
|
|
}
|
|
</script>
|
|
}
|