Fix invoice re-creation after void; add payment terms selector and shop supplies line
- Voided invoices no longer block creating a new invoice for the same job: voided invoice's JobId FK is cleared so the unique index slot is freed for the replacement - Invoice Details view shows voided invoices as history rather than hiding them - Payment terms: standardized SelectList (Due on Receipt, Net 15/30/45/60/90, 2% 10 Net 30, COD) with custom-term preservation; invoice-due-date.js auto-updates Due Date on term change - Shop supplies on direct (no-quote) jobs: InvoicesController derives the shop supplies line from the company rate when the job has no source quote to read the pre-agreed amount from - Job entity: ShopSuppliesAmount + ShopSuppliesPercent fields preserved through job lifecycle - Migration: AddShopSuppliesAmountToJob Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -175,11 +175,11 @@
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Payment Terms"
|
||||
data-bs-content="Free-text field that prints on the invoice (e.g., 'Net 30', 'Due on Receipt', '2% 10 Net 30'). Pre-filled from the customer's default payment terms. Changing it here only affects this invoice.">
|
||||
data-bs-content="Prints on the invoice. Pre-filled from your App Defaults. Changing it here only affects this invoice.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
<input asp-for="Terms" class="form-control" placeholder="e.g. Net 30" />
|
||||
<select asp-for="Terms" asp-items="ViewBag.PaymentTermsOptions" class="form-select"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -446,6 +446,7 @@
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script src="~/js/invoice-due-date.js"></script>
|
||||
<script>
|
||||
let itemCount = @Model.InvoiceItems.Count;
|
||||
const merchandiseItems = @Html.Raw(ViewBag.MerchandiseItems ?? "[]");
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
var statusDisplay = InvoicesController.GetStatusDisplay(Model.Status);
|
||||
var isDraft = Model.Status == InvoiceStatus.Draft;
|
||||
var isVoided = Model.Status == InvoiceStatus.Voided || Model.Status == InvoiceStatus.WrittenOff;
|
||||
var canEdit = Model.Status is InvoiceStatus.Draft or InvoiceStatus.Sent or InvoiceStatus.Overdue;
|
||||
var canPay = !isVoided && Model.BalanceDue > 0;
|
||||
var canResend = !isDraft && !isVoided && Model.Status != InvoiceStatus.Paid;
|
||||
var hasEmail = !string.IsNullOrWhiteSpace(Model.CustomerEmail);
|
||||
@@ -30,12 +31,16 @@
|
||||
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-end gap-2 mb-4">
|
||||
@if (isDraft)
|
||||
@if (canEdit)
|
||||
{
|
||||
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-warning">
|
||||
<i class="bi bi-pencil me-2"></i>Edit
|
||||
</a>
|
||||
}
|
||||
<a asp-action="DownloadPdf" asp-route-id="@Model.Id" asp-route-inline="true"
|
||||
class="btn btn-outline-secondary" target="_blank" rel="noopener">
|
||||
<i class="bi bi-printer me-2"></i>Print
|
||||
</a>
|
||||
<a asp-action="DownloadPdf" asp-route-id="@Model.Id" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-file-pdf me-2"></i>PDF
|
||||
</a>
|
||||
@@ -64,7 +69,7 @@
|
||||
<div class="alert alert-warning d-flex align-items-center gap-2 mb-4">
|
||||
<i class="bi bi-envelope-slash fs-5"></i>
|
||||
<span>
|
||||
<strong>@Model.CustomerName</strong> has no email address on file — email buttons are hidden.
|
||||
<strong>@Model.CustomerName</strong> has no email address on file — you'll be prompted to enter one when sending.
|
||||
<a asp-controller="Customers" asp-action="Edit" asp-route-id="@Model.CustomerId" class="alert-link">Add one in customer settings</a>.
|
||||
</span>
|
||||
</div>
|
||||
@@ -566,31 +571,37 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
@if (isDraft)
|
||||
@if (canEdit)
|
||||
{
|
||||
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-outline-primary">
|
||||
<i class="bi bi-pencil me-2"></i>Edit Invoice
|
||||
</a>
|
||||
@if (hasEmail)
|
||||
{
|
||||
<form id="sendInvoiceForm" asp-action="Send" asp-route-id="@Model.Id" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
@if (emailOptedOut)
|
||||
{
|
||||
<button type="button" class="btn btn-primary w-100" disabled
|
||||
title="Email notifications are turned off for this customer">
|
||||
<i class="bi bi-send me-2"></i>Send Invoice
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="button" class="btn btn-primary w-100"
|
||||
data-bs-toggle="modal" data-bs-target="#sendInvoiceModal">
|
||||
<i class="bi bi-send me-2"></i>Send Invoice
|
||||
</button>
|
||||
}
|
||||
</form>
|
||||
}
|
||||
<form id="sendInvoiceForm" asp-action="Send" asp-route-id="@Model.Id" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="overrideEmail" id="sendInvoiceOverrideEmail" value="" />
|
||||
@if (emailOptedOut)
|
||||
{
|
||||
<button type="button" class="btn btn-primary w-100" disabled
|
||||
title="Email notifications are turned off for this customer">
|
||||
<i class="bi bi-send me-2"></i>Send Invoice
|
||||
</button>
|
||||
}
|
||||
else if (hasEmail)
|
||||
{
|
||||
<button type="button" class="btn btn-primary w-100"
|
||||
data-bs-toggle="modal" data-bs-target="#sendInvoiceModal">
|
||||
<i class="bi bi-send me-2"></i>Send Invoice
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="button" class="btn btn-primary w-100"
|
||||
data-bs-toggle="modal" data-bs-target="#sendToAdHocEmailModal"
|
||||
onclick="document.getElementById('adHocEmailMode').value='send'">
|
||||
<i class="bi bi-send me-2"></i>Send Invoice
|
||||
</button>
|
||||
}
|
||||
</form>
|
||||
}
|
||||
@if (canPay)
|
||||
{
|
||||
@@ -598,12 +609,23 @@
|
||||
<i class="bi bi-cash me-2"></i>Record Payment
|
||||
</button>
|
||||
}
|
||||
<a asp-action="DownloadPdf" asp-route-id="@Model.Id" asp-route-inline="true"
|
||||
class="btn btn-outline-secondary" target="_blank" rel="noopener">
|
||||
<i class="bi bi-printer me-2"></i>Print
|
||||
</a>
|
||||
<a asp-action="DownloadPdf" asp-route-id="@Model.Id" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-file-pdf me-2"></i>Download PDF
|
||||
</a>
|
||||
@if (canResend && hasEmail)
|
||||
@if (canResend)
|
||||
{
|
||||
@if (emailOptedOut)
|
||||
@if (!hasEmail)
|
||||
{
|
||||
<button type="button" class="btn btn-outline-primary"
|
||||
data-bs-toggle="modal" data-bs-target="#sendToAdHocEmailModal">
|
||||
<i class="bi bi-send me-2"></i>Send Invoice
|
||||
</button>
|
||||
}
|
||||
else if (emailOptedOut)
|
||||
{
|
||||
<button type="button" class="btn btn-outline-primary" disabled
|
||||
title="Email notifications are turned off for this customer">
|
||||
@@ -978,6 +1000,34 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Send to Ad-hoc Email Modal -->
|
||||
<div class="modal fade" id="sendToAdHocEmailModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-send me-2"></i>Send Invoice</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-muted mb-3">No email address is on file for this customer. Enter an address below to send the invoice.</p>
|
||||
<div class="mb-3">
|
||||
<label for="adHocEmailInput" class="form-label fw-medium">Send To</label>
|
||||
<input type="email" id="adHocEmailInput" class="form-control" placeholder="recipient@example.com" />
|
||||
<div class="form-text">This address will not be saved to the customer record.</div>
|
||||
</div>
|
||||
<div id="adHocEmailError" class="alert alert-danger alert-permanent d-none py-2"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<input type="hidden" id="adHocEmailMode" value="resend" />
|
||||
<button type="button" class="btn btn-primary" onclick="sendToAdHocEmail(@Model.Id)">
|
||||
<i class="bi bi-send me-1"></i>Send Invoice
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Re-send Invoice Modal (AJAX) -->
|
||||
<div class="modal fade" id="resendInvoiceModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
@@ -1299,7 +1349,27 @@
|
||||
}, 400);
|
||||
}
|
||||
|
||||
function resendInvoice(invoiceId) {
|
||||
function sendToAdHocEmail(invoiceId) {
|
||||
const email = (document.getElementById('adHocEmailInput').value ?? '').trim();
|
||||
const errDiv = document.getElementById('adHocEmailError');
|
||||
if (!email || !email.includes('@@')) {
|
||||
errDiv.textContent = 'Please enter a valid email address.';
|
||||
errDiv.classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
errDiv.classList.add('d-none');
|
||||
bootstrap.Modal.getInstance(document.getElementById('sendToAdHocEmailModal'))?.hide();
|
||||
|
||||
const mode = document.getElementById('adHocEmailMode')?.value ?? 'resend';
|
||||
if (mode === 'send') {
|
||||
document.getElementById('sendInvoiceOverrideEmail').value = email;
|
||||
document.getElementById('sendInvoiceForm').submit();
|
||||
} else {
|
||||
resendInvoice(invoiceId, email);
|
||||
}
|
||||
}
|
||||
|
||||
function resendInvoice(invoiceId, overrideEmail) {
|
||||
document.getElementById('resendInvoiceSending').classList.remove('d-none');
|
||||
document.getElementById('resendInvoiceResult').classList.add('d-none');
|
||||
document.getElementById('resendInvoiceFooter').classList.add('d-none');
|
||||
@@ -1309,8 +1379,10 @@
|
||||
modal.show();
|
||||
|
||||
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value ?? '';
|
||||
let url = '@Url.Action("ResendInvoice", "Invoices")?id=' + invoiceId;
|
||||
if (overrideEmail) url += '&overrideEmail=' + encodeURIComponent(overrideEmail);
|
||||
|
||||
fetch('@Url.Action("ResendInvoice", "Invoices")?id=' + invoiceId, {
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'RequestVerificationToken': token, 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
var invoiceId = (int)(ViewBag.InvoiceId ?? 0);
|
||||
var jobNumber = ViewBag.JobNumber as string;
|
||||
var customerName = ViewBag.CustomerName as string;
|
||||
var canResend = ViewBag.CanResend == true;
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
@@ -37,7 +38,7 @@
|
||||
<a tabindex="0" class="help-icon" role="button"
|
||||
data-bs-toggle="popover" data-bs-placement="right" data-bs-trigger="focus"
|
||||
data-bs-title="Invoice Details"
|
||||
data-bs-content="Invoice Date is the date of issue and the reference for payment terms. Due Date drives overdue status and A/R aging. Payment Terms prints on the invoice — changing it here only affects this invoice. Only Draft invoices can be edited; sending locks the invoice.">
|
||||
data-bs-content="Invoice Date is the date of issue and the reference for payment terms. Due Date drives overdue status and A/R aging. Payment Terms prints on the invoice — changing it here only affects this invoice. Draft, Sent, and Overdue invoices can be edited; Paid and Partially Paid invoices are locked.">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -64,7 +65,7 @@
|
||||
<div class="row g-3 mt-1">
|
||||
<div class="col-md-12">
|
||||
<label asp-for="Terms" class="form-label fw-semibold">Payment Terms</label>
|
||||
<input asp-for="Terms" class="form-control" placeholder="e.g. Net 30" />
|
||||
<select asp-for="Terms" asp-items="ViewBag.PaymentTermsOptions" class="form-select"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -234,6 +235,15 @@
|
||||
<!-- Actions -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body d-grid gap-2">
|
||||
@if (canResend)
|
||||
{
|
||||
<div class="form-check mb-1">
|
||||
<input class="form-check-input" type="checkbox" name="resendToCustomer" value="true" id="resendCheck" />
|
||||
<label class="form-check-label small" for="resendCheck">
|
||||
<i class="bi bi-send me-1"></i>Re-send updated invoice to customer
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle me-2"></i>Save Changes
|
||||
</button>
|
||||
@@ -242,9 +252,10 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-footer border-0 pt-0">
|
||||
<div class="alert alert-warning mb-0 small py-2">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||
Only <strong>Draft</strong> invoices can be edited. Send the invoice to lock it.
|
||||
<div class="alert alert-info mb-0 small py-2">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
<strong>Draft, Sent,</strong> and <strong>Overdue</strong> invoices can be edited.
|
||||
Paid invoices are locked.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user