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:
2026-05-08 20:47:34 -04:00
parent fc35fd123c
commit 3278152d83
10 changed files with 9972 additions and 74 deletions
@@ -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);
}
});
})();