Fix payment info on PDF; always show terms/pay-by; bank details conditional

- Payment terms (default 7 days) and pay-by date are now always visible in the
  form and always rendered in the invoice preview and PDF when pTerm > 0
- Bank account details moved into a separate #bank-section that is shown only
  when "Other" is selected or when the predefined charge-to has bank info in config
- hide-payment-info: yes now only suppresses bank details, not terms/pay-by
- Example NGO in config.yml now includes UK bank info (NatWest IBAN + BIC) which
  auto-fills and locks the bank section when that recipient is selected
- calcPayBy() is called on form init so the default 7-day pay-by date shows immediately

https://claude.ai/code/session_015iyCBgoTXNNqaErR287U1u
This commit is contained in:
Claude 2026-05-19 09:07:24 +00:00
parent 72c2c9e637
commit e4cff52909
No known key found for this signature in database
2 changed files with 94 additions and 51 deletions

View file

@ -131,6 +131,12 @@ charge-to:
vat-id: "GB123456789"
reg-no: "01234567"
currency: GBP
pay-account-holder: Example Non-Profit Organisation
pay-account-no: "GB29 NWBK 6016 1331 9268 19"
pay-bic: "NWBKGB2L"
pay-bank-address1: "NatWest, 1 Princes Street"
pay-bank-address2: "London EC2R 8PB"
pay-ref: ""
# ── Project codes ──────────────────────────────────────────────────────────────
project-codes:

View file

@ -296,9 +296,10 @@
.pay-by-row label { font-size: 12px; color: var(--text-muted); white-space: nowrap; }
#paybydisp { font-size: 13px; font-weight: 600; color: var(--text); }
/* ── Locked charge-to fields ────────────────────────────────────────────── */
/* ── Locked charge-to / bank fields ─────────────────────────────────────── */
#ct-fields.locked input,
#ct-fields.locked select {
#ct-fields.locked select,
#bank-section.locked input {
background: #f8f9fa;
color: var(--text-muted);
border-color: var(--border-light);
@ -562,17 +563,18 @@ function buildForm() {
<div class="fi"><label id="lbl-creg">${t("registration-no")}:</label>
<input id="creg" type="text"></div>
</div>
<div id="payment-panel" style="display:none">
<div id="payment-panel">
<div class="pay-section-title" id="lbl-pay-sec">${t("payment")}</div>
<div class="pay-terms-row">
<label id="lbl-pterm">${t("payment-terms")}:</label>
<input id="pterm" type="number" min="0" oninput="calcPayBy()">
<input id="pterm" type="number" min="0" value="7" oninput="calcPayBy()">
<span id="lbl-days">${t("payment-days")}</span>
</div>
<div class="pay-by-row">
<label id="lbl-paybyl">${t("pay-by")}:</label>
<span id="paybydisp"></span>
</div>
<div id="bank-section" style="display:none">
<div class="fg"><label id="lbl-pacct">${t("account-holder")}</label>
<input id="pacct" type="text"></div>
<div class="fg"><label id="lbl-piban">${t("account-no")}</label>
@ -587,6 +589,7 @@ function buildForm() {
</div>
</div>
</div>
</div>
<div id="lines-card">
<div class="card-title" id="sec-lines">${t("invoice-lines")}</div>
@ -643,6 +646,7 @@ function buildForm() {
document.getElementById("paid-inp").addEventListener("input", calcTotals);
document.getElementById("the-form").addEventListener("submit", e => { e.preventDefault(); generateInvoice(); });
document.querySelectorAll("[data-ls]").forEach(el => el.addEventListener("change", saveStorage));
calcPayBy(); // compute initial pay-by from default 7-day term
}
// ── Fill charge-to ────────────────────────────────────────────────────────────
@ -651,12 +655,20 @@ function fillChargeTo(v) {
const fields = document.getElementById("ct-fields");
const hidePayment = cfg["hide-payment-info"] === true || cfg["hide-payment-info"] === "yes";
const ppanel = document.getElementById("payment-panel");
if (ppanel) ppanel.style.display = (!hidePayment && v === "__other__") ? "" : "none";
const bsec = document.getElementById("bank-section");
if (v === "" || v === "__other__") {
if (v === "") ["ctn","ca1","ca2","ca3","ca4","cc","cph","cem","cvat","creg"].forEach(id => f(id, ""));
fields?.classList.remove("locked");
if (bsec) {
bsec.classList.remove("locked");
if (!hidePayment && v === "__other__") {
bsec.style.display = "";
} else {
bsec.style.display = "none";
["pacct","piban","pbic","pbadr1","pbadr2","pref"].forEach(id => f(id, ""));
}
}
return;
}
const ct = (cfg["charge-to"] || [])[+v];
@ -668,6 +680,29 @@ function fillChargeTo(v) {
f("cvat", ct["vat-id"]); f("creg", ct["reg-no"]);
fields?.classList.add("locked");
// Fill bank section from config if provided
if (bsec && !hidePayment) {
const hasBank = ct["pay-account-holder"] || ct["pay-account-no"] || ct["pay-bic"];
if (hasBank) {
f("pacct", ct["pay-account-holder"]);
f("piban", ct["pay-account-no"]);
f("pbic", ct["pay-bic"]);
f("pbadr1", ct["pay-bank-address1"]);
f("pbadr2", ct["pay-bank-address2"]);
f("pref", ct["pay-ref"]);
bsec.style.display = "";
bsec.classList.add("locked");
} else {
bsec.style.display = "none";
["pacct","piban","pbic","pbadr1","pbadr2","pref"].forEach(id => f(id, ""));
bsec.classList.remove("locked");
}
} else if (bsec) {
bsec.style.display = "none";
["pacct","piban","pbic","pbadr1","pbadr2","pref"].forEach(id => f(id, ""));
bsec.classList.remove("locked");
}
// Auto-set invoice currency from recipient config
if (ct.currency) {
const icurEl = document.getElementById("icur");
@ -1050,17 +1085,17 @@ function gatherData(renderLang) {
const ctCntry= COUNTRY_MAP[g("cc")] || "";
const ctPh = g("cph"), ctEm = g("cem"), ctVat = g("cvat"), ctReg = g("creg");
const ppanel = document.getElementById("payment-panel");
const payVisible = ppanel && ppanel.style.display !== "none";
const pTerm = payVisible ? (parseInt(document.getElementById("pterm")?.value) || 0) : 0;
const pPayBy = payVisible ? (document.getElementById("paybydisp")?.textContent || "") : "";
const pAcct = payVisible ? g("pacct") : "";
const pIban = payVisible ? g("piban") : "";
const pBic = payVisible ? g("pbic") : "";
const pBadr1 = payVisible ? g("pbadr1"): "";
const pBadr2 = payVisible ? g("pbadr2"): "";
const pRef = payVisible ? g("pref") : "";
const hasPayment = payVisible && !!(pAcct || pIban || pBic || pBadr1 || pRef || pTerm > 0);
const pTerm = parseInt(document.getElementById("pterm")?.value) || 0;
const pPayBy = document.getElementById("paybydisp")?.textContent || "";
const bsec = document.getElementById("bank-section");
const bankVisible = bsec && bsec.style.display !== "none";
const pAcct = bankVisible ? g("pacct") : "";
const pIban = bankVisible ? g("piban") : "";
const pBic = bankVisible ? g("pbic") : "";
const pBadr1 = bankVisible ? g("pbadr1"): "";
const pBadr2 = bankVisible ? g("pbadr2"): "";
const pRef = bankVisible ? g("pref") : "";
const hasBank = !!(pAcct || pIban || pBic || pBadr1 || pRef);
let sub = 0;
const rows = [];
@ -1115,7 +1150,7 @@ function gatherData(renderLang) {
return { dl, td, sName, sAddr, sCntry, sPh, sEm, iDate, pCode, iNo, iCur,
ctName, ctAddr, ctCntry, ctPh, ctEm, ctVat, ctReg,
pTerm, pPayBy, pAcct, pIban, pBic, pBadr1, pBadr2, pRef, hasPayment,
pTerm, pPayBy, pAcct, pIban, pBic, pBadr1, pBadr2, pRef, hasBank,
rows, sub, taxes, totalTax, paid, toPay };
}
@ -1198,17 +1233,17 @@ function buildPreviewHTML() {
<td class="tv">${fmt(toPay)}</td>
</tr>
</table>
${hasPayment ? `
${(pTerm > 0 || hasBank) ? `
<div class="d-payment">
<div class="pay-title">${h(td("payment"))}</div>
${pTerm > 0 ? `<div class="pay-terms">${h(td("payment-terms"))}: <strong>${pTerm}</strong> ${h(td("payment-days"))}${pPayBy ? ` &mdash; ${h(td("pay-by"))}: <strong>${h(pPayBy)}</strong>` : ""}</div>` : ""}
<div class="pay-grid">
${hasBank ? `<div class="pay-grid">
${pAcct ? `<span class="pl">${h(td("account-holder"))}</span><span class="pv">${h(pAcct)}</span>` : ""}
${pIban ? `<span class="pl">${h(td("account-no"))}</span><span class="pv">${h(pIban)}</span>` : ""}
${pBic ? `<span class="pl">${h(td("bank-bic"))}</span><span class="pv">${h(pBic)}</span>` : ""}
${pBadr1 || pBadr2 ? `<span class="pl">${h(td("bank-address"))}</span><span class="pv">${[pBadr1,pBadr2].filter(Boolean).map(l=>h(l)).join("<br>")}</span>` : ""}
</div>
${pRef ? `<div class="pay-ref">${h(td("payment-ref"))}: <strong>${h(pRef)}</strong></div>` : ""}
${pRef ? `<div class="pay-ref">${h(td("payment-ref"))}: <strong>${h(pRef)}</strong></div>` : ""}` : ""}
</div>` : ""}`;
}
@ -1421,7 +1456,7 @@ function buildPDF() {
y += 9;
// ── PAYMENT DETAILS ───────────────────────────────────────────────────────
if (hasPayment) {
if (pTerm > 0 || hasBank) {
y += 8;
if (y + 8 > PH - 15) { doc.addPage(); y = MT; }
dc(209,213,219); doc.setLineWidth(0.4);
@ -1434,6 +1469,7 @@ function buildPDF() {
fn(8.5); tc(17,24,39); tL(termsStr, ML, y); y += 5;
}
if (hasBank) {
const LBL = 50;
const payRows = [
pAcct ? [td("account-holder"), pAcct] : null,
@ -1456,6 +1492,7 @@ function buildPDF() {
fb(9); tc(17,24,39); tL(pRef, ML + LBL, y);
}
}
}
// ── Save ──────────────────────────────────────────────────────────────────
doc.save(iNo ? `invoice-${iNo}.pdf` : "invoice.pdf");