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:
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Auto-calculates invoice Due Date from Invoice Date + Payment Terms.
|
||||
* Parses common terms formats: "Net 30", "N/15", "Due on Receipt", "COD", "2% 10 Net 30".
|
||||
* Only fires when Terms or Invoice Date changes; user can always override the Due Date field.
|
||||
*/
|
||||
(function () {
|
||||
/// <summary>
|
||||
/// Extracts the net payment days from a free-text terms string.
|
||||
/// Returns null when the string can't be parsed (due date is left unchanged).
|
||||
/// </summary>
|
||||
function parseDays(terms) {
|
||||
if (!terms || !terms.trim()) return null;
|
||||
const t = terms.trim().toLowerCase();
|
||||
if (/\b(receipt|due\s*now|cod|immediate)\b/.test(t)) return 0;
|
||||
// "Net N" or "N/N" (e.g., "2% 10 Net 30" or "N/30")
|
||||
let m = t.match(/\bnet\s+(\d+)/) || t.match(/\bn\/(\d+)/);
|
||||
if (m) return parseInt(m[1], 10);
|
||||
// Plain number (e.g., "30 days", "30")
|
||||
m = t.match(/\b(\d+)\b/);
|
||||
if (m) return parseInt(m[1], 10);
|
||||
return null;
|
||||
}
|
||||
|
||||
function recalcDueDate() {
|
||||
const termsEl = document.getElementById('Terms');
|
||||
const invoiceDateEl = document.getElementById('InvoiceDate');
|
||||
const dueDateEl = document.getElementById('DueDate');
|
||||
if (!termsEl || !invoiceDateEl || !dueDateEl) return;
|
||||
|
||||
const days = parseDays(termsEl.value);
|
||||
if (days === null) return;
|
||||
|
||||
const rawDate = invoiceDateEl.value;
|
||||
if (!rawDate) return;
|
||||
|
||||
// Parse as local date to avoid UTC-offset shifting the day
|
||||
const [y, mo, d] = rawDate.split('-').map(Number);
|
||||
const due = new Date(y, mo - 1, d + days);
|
||||
const yyyy = due.getFullYear();
|
||||
const mm = String(due.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(due.getDate()).padStart(2, '0');
|
||||
dueDateEl.value = `${yyyy}-${mm}-${dd}`;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const termsEl = document.getElementById('Terms');
|
||||
const invoiceDateEl = document.getElementById('InvoiceDate');
|
||||
if (termsEl) {
|
||||
termsEl.addEventListener('change', recalcDueDate);
|
||||
termsEl.addEventListener('blur', recalcDueDate);
|
||||
}
|
||||
if (invoiceDateEl) {
|
||||
invoiceDateEl.addEventListener('change', recalcDueDate);
|
||||
}
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user