mirror of
https://github.com/kbenestad/invoice.git
synced 2026-06-18 16:14:33 +00:00
Configurable date/paper format; multi-line dynamic tax system
Date format - Replace hardcoded toLocaleDateString with a config-driven formatter - date-format key in config (default "d MMMM YYYY") supports tokens: d, dd, M, MM, MMM, MMMM, YY, YYYY (regex replace, longest match first) Paper format - paper-format key in config: "a4" (default) or "letter" - Passed directly to jsPDF; page dimensions adjusted accordingly Tax system redesign - Remove single-select tax-rates from config; add tax-types list, each with a key and per-language labels (VAT, GST, Sales Tax, Withholding, Other) - Totals section now has a dynamic tax-tbody: each row has a number field, a % / Amount selector, and a tax-type label selector; rows are added with "+ Add new tax" and removed with × - calcTotals iterates tLines: % rows compute sub × (val/100), Amount rows use the fixed value directly; each row shows its calculated amount live - gatherData collects all tax lines into a taxes[] array with lineLabel (e.g. "VAT 7%" or "GST (50.00)") and amt; both preview and PDF render one row per tax entry - relabel() rebuilds tax-line dropdowns on language switch https://claude.ai/code/session_015iyCBgoTXNNqaErR287U1u
This commit is contained in:
parent
f90718ba34
commit
15addbeae8
2 changed files with 206 additions and 86 deletions
|
|
@ -21,33 +21,45 @@ languages:
|
||||||
name: Norsk
|
name: Norsk
|
||||||
direction: ltr
|
direction: ltr
|
||||||
|
|
||||||
# ── Tax / VAT options ──────────────────────────────────────────────────────────
|
# ── Date and paper format ─────────────────────────────────────────────────────
|
||||||
tax-rates:
|
# Date tokens: d=day, dd=day(0-padded), M=month number, MM=month(0-padded),
|
||||||
- label-en: "No Tax (0%)"
|
# MMM=short month name, MMMM=full month name, YY=2-digit year, YYYY=4-digit year
|
||||||
label-de: "Keine Steuer (0%)"
|
date-format: "d MMMM YYYY"
|
||||||
label-fr: "Sans taxe (0%)"
|
# paper-format: a4 or letter
|
||||||
label-no: "Ingen mva (0%)"
|
paper-format: a4
|
||||||
rate: 0
|
|
||||||
- label-en: "VAT 5%"
|
# ── Tax types (user builds tax lines; these are the label options) ─────────────
|
||||||
label-de: "MwSt. 5%"
|
tax-types:
|
||||||
label-fr: "TVA 5%"
|
- key: vat
|
||||||
label-no: "Mva 5%"
|
labels:
|
||||||
rate: 5
|
en: VAT
|
||||||
- label-en: "VAT 10%"
|
de: MwSt.
|
||||||
label-de: "MwSt. 10%"
|
fr: TVA
|
||||||
label-fr: "TVA 10%"
|
"no": MVA
|
||||||
label-no: "Mva 10%"
|
- key: gst
|
||||||
rate: 10
|
labels:
|
||||||
- label-en: "VAT 20%"
|
en: GST
|
||||||
label-de: "MwSt. 20%"
|
de: GST
|
||||||
label-fr: "TVA 20%"
|
fr: TPS
|
||||||
label-no: "Mva 20%"
|
"no": GST
|
||||||
rate: 20
|
- key: sales-tax
|
||||||
- label-en: "VAT 25%"
|
labels:
|
||||||
label-de: "MwSt. 25%"
|
en: Sales Tax
|
||||||
label-fr: "TVA 25%"
|
de: Umsatzsteuer
|
||||||
label-no: "Mva 25%"
|
fr: Taxe de vente
|
||||||
rate: 25
|
"no": Omsetningsavgift
|
||||||
|
- key: withholding
|
||||||
|
labels:
|
||||||
|
en: Withholding Tax
|
||||||
|
de: Quellensteuer
|
||||||
|
fr: "Retenue à la source"
|
||||||
|
"no": Kildeskatt
|
||||||
|
- key: other
|
||||||
|
labels:
|
||||||
|
en: Other Tax
|
||||||
|
de: Sonstige Steuer
|
||||||
|
fr: Autre taxe
|
||||||
|
"no": Annen skatt
|
||||||
|
|
||||||
# ── Units of measure ──────────────────────────────────────────────────────────
|
# ── Units of measure ──────────────────────────────────────────────────────────
|
||||||
uom:
|
uom:
|
||||||
|
|
@ -396,6 +408,21 @@ translations:
|
||||||
de: Rechnungswährung
|
de: Rechnungswährung
|
||||||
fr: Devise de facturation
|
fr: Devise de facturation
|
||||||
"no": Fakturavaluta
|
"no": Fakturavaluta
|
||||||
|
add-tax:
|
||||||
|
en: "+ Add new tax"
|
||||||
|
de: "+ Neue Steuer hinzufügen"
|
||||||
|
fr: "+ Ajouter une taxe"
|
||||||
|
"no": "+ Legg til skatt"
|
||||||
|
tax-pct:
|
||||||
|
en: "%"
|
||||||
|
de: "%"
|
||||||
|
fr: "%"
|
||||||
|
"no": "%"
|
||||||
|
tax-amount:
|
||||||
|
en: Amount
|
||||||
|
de: Betrag
|
||||||
|
fr: Montant
|
||||||
|
"no": Beløp
|
||||||
converted-from:
|
converted-from:
|
||||||
en: Converted from
|
en: Converted from
|
||||||
de: Umgerechnet aus
|
de: Umgerechnet aus
|
||||||
|
|
|
||||||
165
app/index.html
165
app/index.html
|
|
@ -160,8 +160,15 @@
|
||||||
.tot-tbl .val { text-align: right; width: 150px; font-size: 13px; font-weight: 600; }
|
.tot-tbl .val { text-align: right; width: 150px; font-size: 13px; font-weight: 600; }
|
||||||
.tot-tbl .final td { background: var(--navy); color: var(--white); font-size: 15px; font-weight: 700; }
|
.tot-tbl .final td { background: var(--navy); color: var(--white); font-size: 15px; font-weight: 700; }
|
||||||
.tot-tbl .final .lbl { color: rgba(255,255,255,.75); }
|
.tot-tbl .final .lbl { color: rgba(255,255,255,.75); }
|
||||||
.tax-sel { width: auto !important; display: inline-block; }
|
|
||||||
.paid-inp { width: 120px !important; text-align: right; }
|
.paid-inp { width: 120px !important; text-align: right; }
|
||||||
|
.tax-inputs { display: flex; align-items: center; gap: 5px; justify-content: flex-end; }
|
||||||
|
.tax-inputs input[type="number"] { width: 68px; }
|
||||||
|
.tax-inputs select { width: auto; }
|
||||||
|
.btn-rm-tax { background: none; color: var(--danger); font-size: 16px; line-height: 1; padding: 1px 4px; border-radius: 3px; }
|
||||||
|
.btn-rm-tax:hover { background: #fef2f2; }
|
||||||
|
.tax-add-cell { text-align: right; padding: 5px 16px !important; }
|
||||||
|
.btn-add-tax { background: none; color: var(--accent); font-size: 12px; font-weight: 500; padding: 3px 8px; border: 1px dashed var(--accent); border-radius: var(--radius); }
|
||||||
|
.btn-add-tax:hover { background: #eff6ff; }
|
||||||
|
|
||||||
/* ── Generate button ────────────────────────────────────────────────────── */
|
/* ── Generate button ────────────────────────────────────────────────────── */
|
||||||
#btn-generate {
|
#btn-generate {
|
||||||
|
|
@ -326,6 +333,8 @@ let cfg = null;
|
||||||
let lang = "en";
|
let lang = "en";
|
||||||
let lid = 0;
|
let lid = 0;
|
||||||
const lines = {};
|
const lines = {};
|
||||||
|
let tlid = 0;
|
||||||
|
const tLines = {};
|
||||||
|
|
||||||
// ── i18n ──────────────────────────────────────────────────────────────────────
|
// ── i18n ──────────────────────────────────────────────────────────────────────
|
||||||
function t(key) {
|
function t(key) {
|
||||||
|
|
@ -334,12 +343,6 @@ function t(key) {
|
||||||
return e[lang] ?? e[cfg["default-code"]] ?? key;
|
return e[lang] ?? e[cfg["default-code"]] ?? key;
|
||||||
}
|
}
|
||||||
|
|
||||||
function tTax(tr, l) {
|
|
||||||
const lk = "label-" + (l || lang);
|
|
||||||
const fb = "label-" + (cfg["default-code"] || "en");
|
|
||||||
return tr[lk] ?? tr[fb] ?? `Tax ${tr.rate}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Numbers ───────────────────────────────────────────────────────────────────
|
// ── Numbers ───────────────────────────────────────────────────────────────────
|
||||||
function fmt(n) {
|
function fmt(n) {
|
||||||
if (n === "" || n == null || isNaN(+n)) return "0.00";
|
if (n === "" || n == null || isNaN(+n)) return "0.00";
|
||||||
|
|
@ -348,10 +351,28 @@ function fmt(n) {
|
||||||
function pn(s) { return parseFloat(String(s ?? 0).replace(/,/g, "")) || 0; }
|
function pn(s) { return parseFloat(String(s ?? 0).replace(/,/g, "")) || 0; }
|
||||||
|
|
||||||
// ── Date ─────────────────────────────────────────────────────────────────────
|
// ── Date ─────────────────────────────────────────────────────────────────────
|
||||||
|
const MONTHS_FULL = ["January","February","March","April","May","June",
|
||||||
|
"July","August","September","October","November","December"];
|
||||||
|
const MONTHS_SHORT = ["Jan","Feb","Mar","Apr","May","Jun",
|
||||||
|
"Jul","Aug","Sep","Oct","Nov","Dec"];
|
||||||
|
|
||||||
function fmtDate(v) {
|
function fmtDate(v) {
|
||||||
if (!v) return "";
|
if (!v) return "";
|
||||||
const [y, m, d] = v.split("-");
|
const [yr, mo, dy] = v.split("-").map(Number);
|
||||||
return new Date(+y, +m - 1, +d).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
|
const pattern = cfg?.["date-format"] || "d MMMM YYYY";
|
||||||
|
return pattern.replace(/YYYY|YY|MMMM|MMM|MM|M|dd|d/g, tok => {
|
||||||
|
switch (tok) {
|
||||||
|
case "YYYY": return yr;
|
||||||
|
case "YY": return String(yr).slice(-2);
|
||||||
|
case "MMMM": return MONTHS_FULL[mo - 1];
|
||||||
|
case "MMM": return MONTHS_SHORT[mo - 1];
|
||||||
|
case "MM": return String(mo).padStart(2, "0");
|
||||||
|
case "M": return mo;
|
||||||
|
case "dd": return String(dy).padStart(2, "0");
|
||||||
|
case "d": return dy;
|
||||||
|
default: return tok;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── HTML escape ───────────────────────────────────────────────────────────────
|
// ── HTML escape ───────────────────────────────────────────────────────────────
|
||||||
|
|
@ -442,8 +463,6 @@ function buildForm() {
|
||||||
|
|
||||||
const pcOpts = (cfg["project-codes"] || []).map(pc => `<option value="${h(pc)}">${h(pc)}</option>`).join("");
|
const pcOpts = (cfg["project-codes"] || []).map(pc => `<option value="${h(pc)}">${h(pc)}</option>`).join("");
|
||||||
const ctOpts = (cfg["charge-to"] || []).map((ct, i) => `<option value="${i}">${h(ct.display || ct.name)}</option>`).join("");
|
const ctOpts = (cfg["charge-to"] || []).map((ct, i) => `<option value="${i}">${h(ct.display || ct.name)}</option>`).join("");
|
||||||
const taxOpts = (cfg["tax-rates"] || []).map((tr, i) =>
|
|
||||||
`<option value="${tr.rate}" ${i === 0 ? "selected" : ""}>${h(tTax(tr))}</option>`).join("");
|
|
||||||
|
|
||||||
document.getElementById("form-root").innerHTML = `
|
document.getElementById("form-root").innerHTML = `
|
||||||
<form id="the-form" novalidate>
|
<form id="the-form" novalidate>
|
||||||
|
|
@ -542,13 +561,18 @@ function buildForm() {
|
||||||
|
|
||||||
<div id="totals-card">
|
<div id="totals-card">
|
||||||
<table class="tot-tbl">
|
<table class="tot-tbl">
|
||||||
|
<tbody id="tot-pre">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="lbl" id="lbl-sub">${t("subtotal")}</td>
|
<td class="lbl" id="lbl-sub">${t("subtotal")}</td>
|
||||||
<td class="val" id="v-sub">0.00</td>
|
<td class="val" id="v-sub">0.00</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tbody id="tax-tbody"></tbody>
|
||||||
|
<tbody id="tot-post">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="lbl"><select id="tax-sel" class="tax-sel">${taxOpts}</select></td>
|
<td colspan="2" class="tax-add-cell">
|
||||||
<td class="val" id="v-tax">0.00</td>
|
<button type="button" class="btn-add-tax" id="btn-add-tax" onclick="addTaxLine()">${t("add-tax")}</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="lbl" id="lbl-paid">${t("paid")}</td>
|
<td class="lbl" id="lbl-paid">${t("paid")}</td>
|
||||||
|
|
@ -558,6 +582,7 @@ function buildForm() {
|
||||||
<td class="lbl" id="lbl-topay">${t("to-pay")}</td>
|
<td class="lbl" id="lbl-topay">${t("to-pay")}</td>
|
||||||
<td class="val" id="v-topay">0.00</td>
|
<td class="val" id="v-topay">0.00</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -568,7 +593,6 @@ function buildForm() {
|
||||||
document.getElementById("pcode-other-wrap").style.display = this.value === "__other__" ? "block" : "none";
|
document.getElementById("pcode-other-wrap").style.display = this.value === "__other__" ? "block" : "none";
|
||||||
});
|
});
|
||||||
document.getElementById("ct-pick").addEventListener("change", e => fillChargeTo(e.target.value));
|
document.getElementById("ct-pick").addEventListener("change", e => fillChargeTo(e.target.value));
|
||||||
document.getElementById("tax-sel").addEventListener("change", calcTotals);
|
|
||||||
document.getElementById("paid-inp").addEventListener("input", calcTotals);
|
document.getElementById("paid-inp").addEventListener("input", calcTotals);
|
||||||
document.getElementById("the-form").addEventListener("submit", e => { e.preventDefault(); generateInvoice(); });
|
document.getElementById("the-form").addEventListener("submit", e => { e.preventDefault(); generateInvoice(); });
|
||||||
document.querySelectorAll("[data-ls]").forEach(el => el.addEventListener("change", saveStorage));
|
document.querySelectorAll("[data-ls]").forEach(el => el.addEventListener("change", saveStorage));
|
||||||
|
|
@ -626,6 +650,47 @@ function currOpts(sel) {
|
||||||
).join("");
|
).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTaxTypeOpts(sel) {
|
||||||
|
return (cfg["tax-types"] || []).map(tt => {
|
||||||
|
const lbl = tt.labels?.[lang] ?? tt.labels?.[cfg["default-code"]] ?? tt.key;
|
||||||
|
return `<option value="${h(tt.key)}" ${tt.key === sel ? "selected" : ""}>${h(lbl)}</option>`;
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTaxLine() {
|
||||||
|
const i = tlid++;
|
||||||
|
tLines[i] = {};
|
||||||
|
const tbody = document.getElementById("tax-tbody");
|
||||||
|
const defaultKey = (cfg["tax-types"]||[])[0]?.key || "";
|
||||||
|
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
tr.className = "tax-row"; tr.id = `tlr-${i}`;
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td class="lbl">
|
||||||
|
<div class="tax-inputs">
|
||||||
|
<input type="number" id="tv-${i}" value="" placeholder="0" min="0" step="any"
|
||||||
|
oninput="calcTotals()" style="width:68px">
|
||||||
|
<select id="tt-${i}" style="width:auto" onchange="calcTotals()">
|
||||||
|
<option value="pct">${t("tax-pct")}</option>
|
||||||
|
<option value="amt">${t("tax-amount")}</option>
|
||||||
|
</select>
|
||||||
|
<select id="tk-${i}" style="width:auto" onchange="calcTotals()">
|
||||||
|
${getTaxTypeOpts(defaultKey)}
|
||||||
|
</select>
|
||||||
|
<button type="button" class="btn-rm-tax" onclick="removeTaxLine(${i})">×</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="val" id="ta-${i}">0.00</td>`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
calcTotals();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTaxLine(i) {
|
||||||
|
document.getElementById(`tlr-${i}`)?.remove();
|
||||||
|
delete tLines[i];
|
||||||
|
calcTotals();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Add / remove line ─────────────────────────────────────────────────────────
|
// ── Add / remove line ─────────────────────────────────────────────────────────
|
||||||
function addLine() {
|
function addLine() {
|
||||||
const i = lid++;
|
const i = lid++;
|
||||||
|
|
@ -753,13 +818,23 @@ function calcTotals() {
|
||||||
sub += pn(document.getElementById(`qty-${i}`)?.value) *
|
sub += pn(document.getElementById(`qty-${i}`)?.value) *
|
||||||
pn(document.getElementById(`price-${i}`)?.value);
|
pn(document.getElementById(`price-${i}`)?.value);
|
||||||
});
|
});
|
||||||
const taxRate = pn(document.getElementById("tax-sel")?.value) / 100;
|
|
||||||
const taxAmt = sub * taxRate;
|
let totalTax = 0;
|
||||||
|
Object.keys(tLines).forEach(i => {
|
||||||
|
const val = pn(document.getElementById(`tv-${i}`)?.value);
|
||||||
|
const type = document.getElementById(`tt-${i}`)?.value || "pct";
|
||||||
|
const amt = type === "pct" ? sub * (val / 100) : val;
|
||||||
|
totalTax += amt;
|
||||||
|
const el = document.getElementById(`ta-${i}`);
|
||||||
|
if (el) el.textContent = fmt(amt);
|
||||||
|
});
|
||||||
|
|
||||||
const paid = pn(document.getElementById("paid-inp")?.value);
|
const paid = pn(document.getElementById("paid-inp")?.value);
|
||||||
const toPay = sub + taxAmt - paid;
|
const toPay = sub + totalTax - paid;
|
||||||
|
|
||||||
const set = (id, v) => { const e = document.getElementById(id); if (e) e.textContent = fmt(v); };
|
const set = (id, v) => { const e = document.getElementById(id); if (e) e.textContent = fmt(v); };
|
||||||
set("v-sub", sub); set("v-tax", taxAmt); set("v-topay", toPay);
|
set("v-sub", sub);
|
||||||
|
set("v-topay", toPay);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── LocalStorage ──────────────────────────────────────────────────────────────
|
// ── LocalStorage ──────────────────────────────────────────────────────────────
|
||||||
|
|
@ -843,13 +918,19 @@ function relabel() {
|
||||||
const genBtn = document.getElementById("btn-generate");
|
const genBtn = document.getElementById("btn-generate");
|
||||||
if (genBtn) genBtn.textContent = t("generate-invoice");
|
if (genBtn) genBtn.textContent = t("generate-invoice");
|
||||||
|
|
||||||
const taxSel = document.getElementById("tax-sel");
|
// Rebuild dynamic tax line dropdowns
|
||||||
if (taxSel) {
|
Object.keys(tLines).forEach(i => {
|
||||||
const cur = taxSel.value;
|
const ttEl = document.getElementById(`tt-${i}`);
|
||||||
taxSel.innerHTML = (cfg["tax-rates"] || []).map(tr =>
|
if (ttEl) {
|
||||||
`<option value="${tr.rate}" ${String(tr.rate) === cur ? "selected" : ""}>${h(tTax(tr))}</option>`
|
const sv = ttEl.value;
|
||||||
).join("");
|
ttEl.innerHTML = `<option value="pct">${t("tax-pct")}</option><option value="amt">${t("tax-amount")}</option>`;
|
||||||
|
ttEl.value = sv;
|
||||||
}
|
}
|
||||||
|
const tkEl = document.getElementById(`tk-${i}`);
|
||||||
|
if (tkEl) { const sv = tkEl.value; tkEl.innerHTML = getTaxTypeOpts(sv); }
|
||||||
|
});
|
||||||
|
const atBtn = document.getElementById("btn-add-tax");
|
||||||
|
if (atBtn) atBtn.textContent = t("add-tax");
|
||||||
|
|
||||||
const ctPick = document.getElementById("ct-pick");
|
const ctPick = document.getElementById("ct-pick");
|
||||||
if (ctPick) {
|
if (ctPick) {
|
||||||
|
|
@ -935,23 +1016,33 @@ function gatherData(renderLang) {
|
||||||
rows.push({ qty, uomLbl, desc, price, tot, fxNote });
|
rows.push({ qty, uomLbl, desc, price, tot, fxNote });
|
||||||
});
|
});
|
||||||
|
|
||||||
const taxRate = pn(document.getElementById("tax-sel")?.value);
|
let totalTax = 0;
|
||||||
const taxAmt = sub * (taxRate / 100);
|
const taxes = [];
|
||||||
|
Object.keys(tLines).sort((a,b)=>+a-+b).forEach(i => {
|
||||||
|
const val = pn(document.getElementById(`tv-${i}`)?.value);
|
||||||
|
const type = document.getElementById(`tt-${i}`)?.value || "pct";
|
||||||
|
const key = document.getElementById(`tk-${i}`)?.value || "";
|
||||||
|
const ttObj = (cfg["tax-types"]||[]).find(x => x.key === key);
|
||||||
|
const label = ttObj?.labels?.[dl] ?? ttObj?.labels?.[cfg["default-code"]] ?? key;
|
||||||
|
const amt = type === "pct" ? sub * (val / 100) : val;
|
||||||
|
totalTax += amt;
|
||||||
|
const lineLabel = type === "pct" ? `${label} ${val}%` : `${label} (${fmt(val)})`;
|
||||||
|
taxes.push({ val, type, key, label, lineLabel, amt });
|
||||||
|
});
|
||||||
|
|
||||||
const paid = pn(document.getElementById("paid-inp")?.value);
|
const paid = pn(document.getElementById("paid-inp")?.value);
|
||||||
const toPay = sub + taxAmt - paid;
|
const toPay = sub + totalTax - paid;
|
||||||
const taxRateObj = (cfg["tax-rates"]||[]).find(r => r.rate == taxRate);
|
|
||||||
const taxLabel = taxRateObj ? tTax(taxRateObj, dl) : `${td("tax")} ${taxRate}%`;
|
|
||||||
|
|
||||||
return { dl, td, sName, sAddr, sCntry, sPh, sEm, iDate, pCode, iNo, iCur,
|
return { dl, td, sName, sAddr, sCntry, sPh, sEm, iDate, pCode, iNo, iCur,
|
||||||
ctName, ctAddr, ctCntry, ctPh, ctEm, ctVat,
|
ctName, ctAddr, ctCntry, ctPh, ctEm, ctVat,
|
||||||
rows, sub, taxRate, taxAmt, taxLabel, paid, toPay };
|
rows, sub, taxes, totalTax, paid, toPay };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Build HTML preview ────────────────────────────────────────────────────────
|
// ── Build HTML preview ────────────────────────────────────────────────────────
|
||||||
function buildPreviewHTML() {
|
function buildPreviewHTML() {
|
||||||
const { td, sName, sAddr, sCntry, sPh, sEm, iDate, pCode, iNo, iCur,
|
const { td, sName, sAddr, sCntry, sPh, sEm, iDate, pCode, iNo, iCur,
|
||||||
ctName, ctAddr, ctCntry, ctPh, ctEm, ctVat,
|
ctName, ctAddr, ctCntry, ctPh, ctEm, ctVat,
|
||||||
rows, sub, taxRate, taxAmt, taxLabel, paid, toPay } = gatherData();
|
rows, sub, taxes, paid, toPay } = gatherData();
|
||||||
|
|
||||||
const linesHTML = rows.map(row => {
|
const linesHTML = rows.map(row => {
|
||||||
const fxLine = row.fxNote
|
const fxLine = row.fxNote
|
||||||
|
|
@ -1016,7 +1107,7 @@ function buildPreviewHTML() {
|
||||||
<td class="tl">${h(td("subtotal"))}</td>
|
<td class="tl">${h(td("subtotal"))}</td>
|
||||||
<td class="tv">${fmt(sub)}</td>
|
<td class="tv">${fmt(sub)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
${taxRate > 0 ? `<tr><td class="sp"></td><td class="tl">${h(taxLabel)}</td><td class="tv">${fmt(taxAmt)}</td></tr>` : ""}
|
${taxes.map(tx => `<tr><td class="sp"></td><td class="tl">${h(tx.lineLabel)}</td><td class="tv">${fmt(tx.amt)}</td></tr>`).join("")}
|
||||||
${paid > 0 ? `<tr><td class="sp"></td><td class="tl">${h(td("paid"))}</td><td class="tv">−${fmt(paid)}</td></tr>` : ""}
|
${paid > 0 ? `<tr><td class="sp"></td><td class="tl">${h(td("paid"))}</td><td class="tv">−${fmt(paid)}</td></tr>` : ""}
|
||||||
<tr class="fin">
|
<tr class="fin">
|
||||||
<td class="sp"></td>
|
<td class="sp"></td>
|
||||||
|
|
@ -1031,10 +1122,12 @@ function buildPDF() {
|
||||||
if (!window.jspdf) { alert("PDF library not loaded — check your internet connection."); return; }
|
if (!window.jspdf) { alert("PDF library not loaded — check your internet connection."); return; }
|
||||||
|
|
||||||
const { jsPDF } = window.jspdf;
|
const { jsPDF } = window.jspdf;
|
||||||
const doc = new jsPDF({ unit: "mm", format: "a4" });
|
const paperFmt = (cfg["paper-format"] || "a4").toLowerCase();
|
||||||
|
const doc = new jsPDF({ unit: "mm", format: paperFmt });
|
||||||
|
|
||||||
// Page geometry
|
// Page geometry
|
||||||
const PW = 210, PH = 297;
|
const PW = paperFmt === "letter" ? 215.9 : 210;
|
||||||
|
const PH = paperFmt === "letter" ? 279.4 : 297;
|
||||||
const ML = 15, MR = 15, MT = 15;
|
const ML = 15, MR = 15, MT = 15;
|
||||||
const CW = PW - ML - MR; // 180mm content width
|
const CW = PW - ML - MR; // 180mm content width
|
||||||
const XR = PW - MR; // 195mm right edge
|
const XR = PW - MR; // 195mm right edge
|
||||||
|
|
@ -1051,7 +1144,7 @@ function buildPDF() {
|
||||||
|
|
||||||
const { dl, td, sName, sAddr, sCntry, sPh, sEm, iDate, pCode, iNo, iCur,
|
const { dl, td, sName, sAddr, sCntry, sPh, sEm, iDate, pCode, iNo, iCur,
|
||||||
ctName, ctAddr, ctCntry, ctPh, ctEm, ctVat,
|
ctName, ctAddr, ctCntry, ctPh, ctEm, ctVat,
|
||||||
rows, sub, taxRate, taxAmt, taxLabel, paid, toPay } = gatherData();
|
rows, sub, taxes, paid, toPay } = gatherData();
|
||||||
|
|
||||||
// ── HEADER ────────────────────────────────────────────────────────────────
|
// ── HEADER ────────────────────────────────────────────────────────────────
|
||||||
let y = MT;
|
let y = MT;
|
||||||
|
|
@ -1209,7 +1302,7 @@ function buildPDF() {
|
||||||
|
|
||||||
const totRows = [
|
const totRows = [
|
||||||
[td("subtotal"), fmt(sub)],
|
[td("subtotal"), fmt(sub)],
|
||||||
...(taxRate > 0 ? [[taxLabel, fmt(taxAmt)]] : []),
|
...taxes.map(tx => [tx.lineLabel, fmt(tx.amt)]),
|
||||||
...(paid > 0 ? [[td("paid"), "−" + fmt(paid)]] : []),
|
...(paid > 0 ? [[td("paid"), "−" + fmt(paid)]] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue