diff --git a/app/config.yml b/app/config.yml
index 38e6132..392fa25 100644
--- a/app/config.yml
+++ b/app/config.yml
@@ -21,6 +21,11 @@ languages:
name: Norsk
direction: ltr
+# ── Payment info visibility ───────────────────────────────────────────────────
+# Set to true to hide the payment info panel entirely (useful if payment info
+# should not appear on invoices, e.g. per company policy).
+hide-payment-info: false
+
# ── Date and paper format ─────────────────────────────────────────────────────
# Date tokens: d=day, dd=day(0-padded), M=month number, MM=month(0-padded),
# MMM=short month name, MMMM=full month name, YY=2-digit year, YYYY=4-digit year
@@ -460,3 +465,48 @@ translations:
de: Rechnungsdetails
fr: Détails de la facture
"no": Fakturaopplysninger
+ payment:
+ en: Payment
+ de: Zahlung
+ fr: Paiement
+ "no": Betaling
+ payment-terms:
+ en: Payment terms
+ de: Zahlungsbedingungen
+ fr: Conditions de paiement
+ "no": Betalingsbetingelser
+ payment-days:
+ en: days
+ de: Tage
+ fr: jours
+ "no": dager
+ pay-by:
+ en: Pay by
+ de: Zahlbar bis
+ fr: "À payer avant le"
+ "no": Betal innen
+ account-holder:
+ en: Account holder
+ de: Kontoinhaber
+ fr: Titulaire du compte
+ "no": Kontohaver
+ account-no:
+ en: "Account no. (BBAN/IBAN)"
+ de: "Kontonummer (BBAN/IBAN)"
+ fr: "N° de compte (BBAN/IBAN)"
+ "no": "Kontonr. (BBAN/IBAN)"
+ bank-bic:
+ en: "Bank / BIC (Swift code)"
+ de: "Bank / BIC (Swift)"
+ fr: "Banque / BIC (code Swift)"
+ "no": "Bank / BIC (Swift-kode)"
+ bank-address:
+ en: Bank address
+ de: Bankadresse
+ fr: Adresse de la banque
+ "no": Bankadresse
+ payment-ref:
+ en: Please use reference
+ de: Bitte Referenz angeben
+ fr: "Merci d'indiquer la référence"
+ "no": Vennligst oppgi referanse
diff --git a/app/index.html b/app/index.html
index cac1a29..9ebd3a4 100644
--- a/app/index.html
+++ b/app/index.html
@@ -264,6 +264,15 @@
.d-lines tbody td.b { font-weight: 600; }
.d-fx-note { font-size: 9.5px; color: #6b7280; margin-top: 3px; }
+ .d-payment { border-top: 1px solid #e5e7eb; margin-top: 20px; padding-top: 14px; }
+ .d-payment .pay-title { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.2px; color: #6b7280; margin-bottom: 8px; }
+ .d-payment .pay-terms { font-size: 10.5px; color: #4b5563; margin-bottom: 8px; }
+ .d-payment .pay-grid { display: grid; grid-template-columns: 140px 1fr; gap: 3px 10px; font-size: 10.5px; margin-bottom: 4px; }
+ .d-payment .pay-grid .pl { color: #6b7280; }
+ .d-payment .pay-grid .pv { color: #111827; font-weight: 500; }
+ .d-payment .pay-ref { margin-top: 8px; font-size: 10.5px; color: #4b5563; }
+ .d-payment .pay-ref strong { color: #111827; }
+
.d-tots { width: 100%; border-collapse: collapse; }
.d-tots td { padding: 5px 10px; font-size: 11.5px; }
.d-tots .sp { width: 55%; }
@@ -273,6 +282,20 @@
.d-tots .fin td { background: var(--navy); color: white; font-size: 14px; font-weight: 700; border-top: 2px solid var(--navy); }
.d-tots .fin .tl { color: rgba(255,255,255,.75); }
+ /* ── Charge-to / Payment two-column layout ──────────────────────────────── */
+ .ct-two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start; }
+ @media (max-width: 720px) { .ct-two-col { grid-template-columns: 1fr; } }
+ #payment-panel { border-left: 2px solid var(--border-light); padding-left: 20px; }
+ @media (max-width: 720px) { #payment-panel { border-left: none; padding-left: 0; border-top: 2px solid var(--border-light); padding-top: 16px; } }
+ .pay-section-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; color: var(--navy); margin-bottom: 10px; }
+ .pay-terms-row { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; flex-wrap: wrap; }
+ .pay-terms-row label { font-size: 12px; color: var(--text-muted); white-space: nowrap; }
+ .pay-terms-row input { width: 64px; }
+ .pay-terms-row span { font-size: 12px; color: var(--text-muted); }
+ .pay-by-row { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; }
+ .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 ────────────────────────────────────────────── */
#ct-fields.locked input,
#ct-fields.locked select {
@@ -516,27 +539,52 @@ function buildForm() {
-
@@ -591,6 +639,7 @@ function buildForm() {
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("idate").addEventListener("change", calcPayBy);
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));
@@ -601,6 +650,10 @@ function fillChargeTo(v) {
const f = (id, val) => { const el = document.getElementById(id); if (el) el.value = val ?? ""; };
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";
+
if (v === "" || v === "__other__") {
if (v === "") ["ctn","ca1","ca2","ca3","ca4","cc","cph","cem","cvat","creg"].forEach(id => f(id, ""));
fields?.classList.remove("locked");
@@ -810,6 +863,21 @@ function calcLine(i) {
calcTotals();
}
+function calcPayBy() {
+ const idate = document.getElementById("idate")?.value;
+ const terms = parseInt(document.getElementById("pterm")?.value) || 0;
+ const el = document.getElementById("paybydisp");
+ if (!el) return;
+ if (idate && terms > 0) {
+ const d = new Date(idate + "T00:00:00");
+ d.setDate(d.getDate() + terms);
+ const iso = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`;
+ el.textContent = fmtDate(iso);
+ } else {
+ el.textContent = "";
+ }
+}
+
function calcTotals() {
let sub = 0;
Object.keys(lines).forEach(i => {
@@ -885,6 +953,8 @@ function relabel() {
"lbl-ctn":"charge-to-name","lbl-ca1":"charge-to-address1","lbl-ca2":"charge-to-address2",
"lbl-ca3":"charge-to-address3","lbl-ca4":"charge-to-address4","lbl-cc":"charge-to-country",
"lbl-cph":"charge-to-phone","lbl-cem":"charge-to-email","lbl-cvat":"vat-id","lbl-creg":"registration-no",
+ "lbl-pay-sec":"payment","lbl-pterm":"payment-terms","lbl-days":"payment-days","lbl-paybyl":"pay-by",
+ "lbl-pacct":"account-holder","lbl-piban":"account-no","lbl-pbic":"bank-bic","lbl-pbadr":"bank-address","lbl-pref":"payment-ref",
"th-qty":"qty","th-uom":"uom","th-desc":"description","th-price":"price","th-tot":"line-total",
"lbl-sub":"subtotal","lbl-paid":"paid","lbl-topay":"to-pay",
};
@@ -980,6 +1050,18 @@ 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);
+
let sub = 0;
const rows = [];
Object.keys(lines).sort((a,b)=>+a-+b).forEach(i => {
@@ -1033,6 +1115,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,
rows, sub, taxes, totalTax, paid, toPay };
}
@@ -1040,6 +1123,7 @@ function gatherData(renderLang) {
function buildPreviewHTML() {
const { 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,
rows, sub, taxes, paid, toPay } = gatherData();
const linesHTML = rows.map(row => {
@@ -1113,7 +1197,19 @@ function buildPreviewHTML() {
${h(td("to-pay"))} |
${fmt(toPay)} |
- `;
+
+ ${hasPayment ? `
+
+
${h(td("payment"))}
+ ${pTerm > 0 ? `
${h(td("payment-terms"))}: ${pTerm} ${h(td("payment-days"))}${pPayBy ? ` — ${h(td("pay-by"))}: ${h(pPayBy)}` : ""}
` : ""}
+
+ ${pAcct ? `${h(td("account-holder"))}${h(pAcct)}` : ""}
+ ${pIban ? `${h(td("account-no"))}${h(pIban)}` : ""}
+ ${pBic ? `${h(td("bank-bic"))}${h(pBic)}` : ""}
+ ${pBadr1 || pBadr2 ? `${h(td("bank-address"))}${[pBadr1,pBadr2].filter(Boolean).map(l=>h(l)).join("
")}` : ""}
+
+ ${pRef ? `
${h(td("payment-ref"))}: ${h(pRef)}
` : ""}
+
` : ""}`;
}
// ── Build PDF with jsPDF + Helvetica ─────────────────────────────────────────
@@ -1143,6 +1239,7 @@ function buildPDF() {
const { 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,
rows, sub, taxes, paid, toPay } = gatherData();
// ── HEADER ────────────────────────────────────────────────────────────────
@@ -1321,6 +1418,44 @@ function buildPDF() {
doc.rect(TX, y, TW, 9, "F");
fn(9); tc(180,195,215); tR(td("to-pay"), TLBX, y+5.8);
fb(11); tc(255,255,255); tR(fmt(toPay), XR-2, y+5.8);
+ y += 9;
+
+ // ── PAYMENT DETAILS ───────────────────────────────────────────────────────
+ if (hasPayment) {
+ y += 8;
+ if (y + 8 > PH - 15) { doc.addPage(); y = MT; }
+ dc(209,213,219); doc.setLineWidth(0.4);
+ doc.line(ML, y, XR, y); y += 5;
+
+ fb(7); tc(107,114,128); tL(td("payment").toUpperCase(), ML, y); y += 5;
+
+ if (pTerm > 0) {
+ const termsStr = `${td("payment-terms")}: ${pTerm} ${td("payment-days")}${pPayBy ? ` ${td("pay-by")}: ${pPayBy}` : ""}`;
+ fn(8.5); tc(17,24,39); tL(termsStr, ML, y); y += 5;
+ }
+
+ const LBL = 50;
+ const payRows = [
+ pAcct ? [td("account-holder"), pAcct] : null,
+ pIban ? [td("account-no"), pIban] : null,
+ pBic ? [td("bank-bic"), pBic] : null,
+ (pBadr1 || pBadr2) ? [td("bank-address"), [pBadr1,pBadr2].filter(Boolean).join(", ")] : null,
+ ].filter(Boolean);
+
+ payRows.forEach(([lbl, val]) => {
+ if (y + 5 > PH - 10) { doc.addPage(); y = MT; }
+ fn(8); tc(107,114,128); tL(lbl + ":", ML, y);
+ fn(8.5); tc(17,24,39); tL(val, ML + LBL, y);
+ y += 4.5;
+ });
+
+ if (pRef) {
+ y += 1;
+ if (y + 6 > PH - 10) { doc.addPage(); y = MT; }
+ fn(8); tc(107,114,128); tL(td("payment-ref") + ":", ML, y);
+ fb(9); tc(17,24,39); tL(pRef, ML + LBL, y);
+ }
+ }
// ── Save ──────────────────────────────────────────────────────────────────
doc.save(iNo ? `invoice-${iNo}.pdf` : "invoice.pdf");