mirror of
https://github.com/kbenestad/invoice.git
synced 2026-06-18 16:14:33 +00:00
Five fixes to invoice form and PDF
- Date picker: change invoice date from month picker to full date picker (type=date, formatted as "19 May 2026" in output) - Invoice currency: add currency selector under invoice date, populated from config.currencies list, saved to localStorage; shown in invoice meta block on both preview and PDF - Recipient currency: add currency field to each charge-to entry in config.yml; selecting a predefined recipient auto-sets invoice currency - Lock predefined recipients: selecting a predefined charge-to entry locks all its fields (pointer-events off + muted style via #ct-fields .locked); switching to Other or clearing unlocks them - Fix foreign-currency exchange rate calculation: the formula was inverted (per / rate instead of per * rate). If 1 USD = 32 local and per-item is USD 100, local price is now correctly 100 × 32 = 3200, not 100 / 32 = 3.125. Fix applied in calcFxFromPer, calcLine display, and gatherData (foreignTot = per × qty, the foreign-currency total). Updated fx note text to the specified format: "Converted from USD: 1 USD = 32.00000 THB. Per item: USD 100.00, line total: USD 500.00" https://claude.ai/code/session_015iyCBgoTXNNqaErR287U1u
This commit is contained in:
parent
f39eed979a
commit
f90718ba34
2 changed files with 90 additions and 38 deletions
|
|
@ -100,6 +100,7 @@ charge-to:
|
||||||
phone: "+1-212-555-0100"
|
phone: "+1-212-555-0100"
|
||||||
email: accounts@acmecorp.example
|
email: accounts@acmecorp.example
|
||||||
vat-id: "US-EIN-12-3456789"
|
vat-id: "US-EIN-12-3456789"
|
||||||
|
currency: USD
|
||||||
- display: Example NGO
|
- display: Example NGO
|
||||||
name: Example Non-Profit Organisation
|
name: Example Non-Profit Organisation
|
||||||
address1: 45 Charity Lane
|
address1: 45 Charity Lane
|
||||||
|
|
@ -110,6 +111,7 @@ charge-to:
|
||||||
phone: "+44 20 7123 4567"
|
phone: "+44 20 7123 4567"
|
||||||
email: finance@examplengo.example
|
email: finance@examplengo.example
|
||||||
vat-id: "GB123456789"
|
vat-id: "GB123456789"
|
||||||
|
currency: GBP
|
||||||
|
|
||||||
# ── Project codes ──────────────────────────────────────────────────────────────
|
# ── Project codes ──────────────────────────────────────────────────────────────
|
||||||
project-codes:
|
project-codes:
|
||||||
|
|
@ -389,6 +391,16 @@ translations:
|
||||||
de: "Nein"
|
de: "Nein"
|
||||||
fr: "Non"
|
fr: "Non"
|
||||||
"no": "Nei"
|
"no": "Nei"
|
||||||
|
invoice-currency:
|
||||||
|
en: Invoice currency
|
||||||
|
de: Rechnungswährung
|
||||||
|
fr: Devise de facturation
|
||||||
|
"no": Fakturavaluta
|
||||||
|
converted-from:
|
||||||
|
en: Converted from
|
||||||
|
de: Umgerechnet aus
|
||||||
|
fr: Converti depuis
|
||||||
|
"no": Konvertert fra
|
||||||
download-pdf:
|
download-pdf:
|
||||||
en: "Download PDF"
|
en: "Download PDF"
|
||||||
de: "PDF herunterladen"
|
de: "PDF herunterladen"
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,16 @@
|
||||||
.d-tots .fin td { background: var(--navy); color: white; font-size: 14px; font-weight: 700; border-top: 2px solid var(--navy); }
|
.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); }
|
.d-tots .fin .tl { color: rgba(255,255,255,.75); }
|
||||||
|
|
||||||
|
/* ── Locked charge-to fields ────────────────────────────────────────────── */
|
||||||
|
#ct-fields.locked input,
|
||||||
|
#ct-fields.locked select {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-color: var(--border-light);
|
||||||
|
pointer-events: none;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Error / loading ────────────────────────────────────────────────────── */
|
/* ── Error / loading ────────────────────────────────────────────────────── */
|
||||||
#loading { padding: 48px; text-align: center; color: var(--text-muted); font-size: 14px; }
|
#loading { padding: 48px; text-align: center; color: var(--text-muted); font-size: 14px; }
|
||||||
.error-box { background: #fef2f2; border: 1px solid #fca5a5; color: #991b1b; padding: 16px 20px; border-radius: var(--radius); margin: 20px 0; font-size: 13px; }
|
.error-box { background: #fef2f2; border: 1px solid #fca5a5; color: #991b1b; padding: 16px 20px; border-radius: var(--radius); margin: 20px 0; font-size: 13px; }
|
||||||
|
|
@ -338,10 +348,10 @@ 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 ─────────────────────────────────────────────────────────────────────
|
||||||
function fmtMonth(v) {
|
function fmtDate(v) {
|
||||||
if (!v) return "";
|
if (!v) return "";
|
||||||
const [y, m] = v.split("-");
|
const [y, m, d] = v.split("-");
|
||||||
return new Date(+y, +m - 1, 1).toLocaleDateString("en-US", { year: "numeric", month: "long" });
|
return new Date(+y, +m - 1, +d).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── HTML escape ───────────────────────────────────────────────────────────────
|
// ── HTML escape ───────────────────────────────────────────────────────────────
|
||||||
|
|
@ -426,7 +436,9 @@ function buildForm() {
|
||||||
document.getElementById("inv-title").textContent = t("invoice");
|
document.getElementById("inv-title").textContent = t("invoice");
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const monthDef = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}`;
|
const dateDef = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
|
||||||
|
const curOpts = (cfg.currencies || []).map((c, i) =>
|
||||||
|
`<option value="${h(c)}" ${i === 0 ? "selected" : ""}>${h(c)}</option>`).join("");
|
||||||
|
|
||||||
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("");
|
||||||
|
|
@ -458,7 +470,9 @@ function buildForm() {
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-title" id="sec-invdet">${t("invoice-details-section")}</div>
|
<div class="card-title" id="sec-invdet">${t("invoice-details-section")}</div>
|
||||||
<div class="fg"><label id="lbl-idate" for="idate">${t("invoice-date")}</label>
|
<div class="fg"><label id="lbl-idate" for="idate">${t("invoice-date")}</label>
|
||||||
<input id="idate" type="month" value="${monthDef}"></div>
|
<input id="idate" type="date" value="${dateDef}"></div>
|
||||||
|
<div class="fg"><label id="lbl-icur" for="icur">${t("invoice-currency")}</label>
|
||||||
|
<select id="icur" data-ls="icur">${curOpts}</select></div>
|
||||||
<div class="fg"><label id="lbl-pcode" for="pcode">${t("project-code")}</label>
|
<div class="fg"><label id="lbl-pcode" for="pcode">${t("project-code")}</label>
|
||||||
<select id="pcode">
|
<select id="pcode">
|
||||||
<option value="">${t("select")}</option>
|
<option value="">${t("select")}</option>
|
||||||
|
|
@ -483,7 +497,7 @@ function buildForm() {
|
||||||
<option value="__other__">${t("other")}</option>
|
<option value="__other__">${t("other")}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="two-col">
|
<div id="ct-fields" class="two-col">
|
||||||
<div>
|
<div>
|
||||||
<div class="fg"><label id="lbl-ctn" for="ctn">${t("charge-to-name")}</label>
|
<div class="fg"><label id="lbl-ctn" for="ctn">${t("charge-to-name")}</label>
|
||||||
<input id="ctn" type="text"></div>
|
<input id="ctn" type="text"></div>
|
||||||
|
|
@ -563,8 +577,11 @@ function buildForm() {
|
||||||
// ── Fill charge-to ────────────────────────────────────────────────────────────
|
// ── Fill charge-to ────────────────────────────────────────────────────────────
|
||||||
function fillChargeTo(v) {
|
function fillChargeTo(v) {
|
||||||
const f = (id, val) => { const el = document.getElementById(id); if (el) el.value = val ?? ""; };
|
const f = (id, val) => { const el = document.getElementById(id); if (el) el.value = val ?? ""; };
|
||||||
|
const fields = document.getElementById("ct-fields");
|
||||||
|
|
||||||
if (v === "" || v === "__other__") {
|
if (v === "" || v === "__other__") {
|
||||||
["ctn","ca1","ca2","ca3","ca4","cc","cph","cem","cvat"].forEach(id => f(id, ""));
|
if (v === "") ["ctn","ca1","ca2","ca3","ca4","cc","cph","cem","cvat"].forEach(id => f(id, ""));
|
||||||
|
fields?.classList.remove("locked");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ct = (cfg["charge-to"] || [])[+v];
|
const ct = (cfg["charge-to"] || [])[+v];
|
||||||
|
|
@ -574,6 +591,13 @@ function fillChargeTo(v) {
|
||||||
f("ca4", ct.address4); f("cc", ct.country);
|
f("ca4", ct.address4); f("cc", ct.country);
|
||||||
f("cph", ct.phone); f("cem", ct.email);
|
f("cph", ct.phone); f("cem", ct.email);
|
||||||
f("cvat", ct["vat-id"]);
|
f("cvat", ct["vat-id"]);
|
||||||
|
fields?.classList.add("locked");
|
||||||
|
|
||||||
|
// Auto-set invoice currency from recipient config
|
||||||
|
if (ct.currency) {
|
||||||
|
const icurEl = document.getElementById("icur");
|
||||||
|
if (icurEl) { icurEl.value = ct.currency; saveStorage(); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Select option helpers ─────────────────────────────────────────────────────
|
// ── Select option helpers ─────────────────────────────────────────────────────
|
||||||
|
|
@ -702,7 +726,8 @@ function calcFxFromPer(i) {
|
||||||
const per = pn(document.getElementById(`fper-${i}`)?.value);
|
const per = pn(document.getElementById(`fper-${i}`)?.value);
|
||||||
const rate = pn(document.getElementById(`frate-${i}`)?.value) || 1;
|
const rate = pn(document.getElementById(`frate-${i}`)?.value) || 1;
|
||||||
const prEl = document.getElementById(`price-${i}`);
|
const prEl = document.getElementById(`price-${i}`);
|
||||||
if (prEl) prEl.value = (per / rate).toFixed(6);
|
// rate = "1 foreign = rate local", so local price = per * rate
|
||||||
|
if (prEl) prEl.value = (per * rate).toFixed(6);
|
||||||
calcLine(i);
|
calcLine(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -714,10 +739,10 @@ function calcLine(i) {
|
||||||
if (el) el.textContent = fmt(qty * price);
|
if (el) el.textContent = fmt(qty * price);
|
||||||
|
|
||||||
if (document.getElementById(`fx-${i}`)?.value === "yes") {
|
if (document.getElementById(`fx-${i}`)?.value === "yes") {
|
||||||
const rate = pn(document.getElementById(`frate-${i}`)?.value) || 1;
|
|
||||||
const per = pn(document.getElementById(`fper-${i}`)?.value);
|
const per = pn(document.getElementById(`fper-${i}`)?.value);
|
||||||
const ltot = document.getElementById(`fltot-${i}`);
|
const ltot = document.getElementById(`fltot-${i}`);
|
||||||
if (ltot) ltot.textContent = fmt((per / rate) * qty);
|
// Show foreign-currency line total (per * qty, still in foreign currency)
|
||||||
|
if (ltot) ltot.textContent = fmt(per * qty);
|
||||||
}
|
}
|
||||||
calcTotals();
|
calcTotals();
|
||||||
}
|
}
|
||||||
|
|
@ -783,7 +808,7 @@ function relabel() {
|
||||||
"lbl-sn":"sender-name","lbl-sa1":"sender-address1","lbl-sa2":"sender-address2",
|
"lbl-sn":"sender-name","lbl-sa1":"sender-address1","lbl-sa2":"sender-address2",
|
||||||
"lbl-sa3":"sender-address3","lbl-sa4":"sender-address4","lbl-sc":"sender-country",
|
"lbl-sa3":"sender-address3","lbl-sa4":"sender-address4","lbl-sc":"sender-country",
|
||||||
"lbl-sp":"sender-phone","lbl-se":"sender-email",
|
"lbl-sp":"sender-phone","lbl-se":"sender-email",
|
||||||
"lbl-idate":"invoice-date","lbl-pcode":"project-code","lbl-ino":"invoice-no",
|
"lbl-idate":"invoice-date","lbl-icur":"invoice-currency","lbl-pcode":"project-code","lbl-ino":"invoice-no",
|
||||||
"lbl-ctn":"charge-to-name","lbl-ca1":"charge-to-address1","lbl-ca2":"charge-to-address2",
|
"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-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-cph":"charge-to-phone","lbl-cem":"charge-to-email","lbl-cvat":"vat-id",
|
||||||
|
|
@ -869,6 +894,7 @@ function gatherData(renderLang) {
|
||||||
const iDate = g("idate");
|
const iDate = g("idate");
|
||||||
const pCode = g("pcode") === "__other__" ? g("pcode-other") : g("pcode");
|
const pCode = g("pcode") === "__other__" ? g("pcode-other") : g("pcode");
|
||||||
const iNo = g("ino");
|
const iNo = g("ino");
|
||||||
|
const iCur = g("icur");
|
||||||
|
|
||||||
const ctName = g("ctn");
|
const ctName = g("ctn");
|
||||||
const ctAddr = [g("ca1"),g("ca2"),g("ca3"),g("ca4")].filter(Boolean);
|
const ctAddr = [g("ca1"),g("ca2"),g("ca3"),g("ca4")].filter(Boolean);
|
||||||
|
|
@ -903,8 +929,8 @@ function gatherData(renderLang) {
|
||||||
const cur = document.getElementById(`fcur-${i}`)?.value || "";
|
const cur = document.getElementById(`fcur-${i}`)?.value || "";
|
||||||
const rate = pn(document.getElementById(`frate-${i}`)?.value) || 1;
|
const rate = pn(document.getElementById(`frate-${i}`)?.value) || 1;
|
||||||
const per = pn(document.getElementById(`fper-${i}`)?.value);
|
const per = pn(document.getElementById(`fper-${i}`)?.value);
|
||||||
const ltot = (per / rate) * qty;
|
const foreignTot = per * qty; // line total in foreign currency
|
||||||
fxNote = { cur, rate, per, ltot, td };
|
fxNote = { cur, rate, per, foreignTot, td };
|
||||||
}
|
}
|
||||||
rows.push({ qty, uomLbl, desc, price, tot, fxNote });
|
rows.push({ qty, uomLbl, desc, price, tot, fxNote });
|
||||||
});
|
});
|
||||||
|
|
@ -916,22 +942,23 @@ function gatherData(renderLang) {
|
||||||
const taxRateObj = (cfg["tax-rates"]||[]).find(r => r.rate == taxRate);
|
const taxRateObj = (cfg["tax-rates"]||[]).find(r => r.rate == taxRate);
|
||||||
const taxLabel = taxRateObj ? tTax(taxRateObj, dl) : `${td("tax")} ${taxRate}%`;
|
const taxLabel = taxRateObj ? tTax(taxRateObj, dl) : `${td("tax")} ${taxRate}%`;
|
||||||
|
|
||||||
return { dl, td, sName, sAddr, sCntry, sPh, sEm, iDate, pCode, iNo,
|
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, taxRate, taxAmt, taxLabel, paid, toPay };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Build HTML preview ────────────────────────────────────────────────────────
|
// ── Build HTML preview ────────────────────────────────────────────────────────
|
||||||
function buildPreviewHTML() {
|
function buildPreviewHTML() {
|
||||||
const { td, sName, sAddr, sCntry, sPh, sEm, iDate, pCode, iNo,
|
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, taxRate, taxAmt, taxLabel, paid, toPay } = gatherData();
|
||||||
|
|
||||||
const linesHTML = rows.map(row => {
|
const linesHTML = rows.map(row => {
|
||||||
const fxLine = row.fxNote
|
const fxLine = row.fxNote
|
||||||
? `<div class="d-fx-note">${h(row.fxNote.td("foreign-currency"))}: `
|
? `<div class="d-fx-note">${h(row.fxNote.td("converted-from"))} ${h(row.fxNote.cur)}: `
|
||||||
+ `${h(row.fxNote.cur)} ${fmt(row.fxNote.per)} / ${row.fxNote.rate} `
|
+ `1 ${h(row.fxNote.cur)} = ${(+row.fxNote.rate).toFixed(5)} ${h(iCur)}. `
|
||||||
+ `× ${row.qty} = ${fmt(row.fxNote.ltot)}</div>`
|
+ `${h(row.fxNote.td("per-item"))}: ${h(row.fxNote.cur)} ${fmt(row.fxNote.per)}, `
|
||||||
|
+ `${h(row.fxNote.td("line-total")).toLowerCase()}: ${h(row.fxNote.cur)} ${fmt(row.fxNote.foreignTot)}</div>`
|
||||||
: "";
|
: "";
|
||||||
const qStr = row.qty % 1 === 0 ? row.qty : fmt(row.qty);
|
const qStr = row.qty % 1 === 0 ? row.qty : fmt(row.qty);
|
||||||
return `<tr>
|
return `<tr>
|
||||||
|
|
@ -955,8 +982,9 @@ function buildPreviewHTML() {
|
||||||
<h1>${h(td("invoice"))}</h1>
|
<h1>${h(td("invoice"))}</h1>
|
||||||
<table class="d-meta">
|
<table class="d-meta">
|
||||||
${iNo ? `<tr><td class="ml">${h(td("invoice-no"))}</td><td class="mv">${h(iNo)}</td></tr>` : ""}
|
${iNo ? `<tr><td class="ml">${h(td("invoice-no"))}</td><td class="mv">${h(iNo)}</td></tr>` : ""}
|
||||||
${iDate ? `<tr><td class="ml">${h(td("invoice-date"))}</td><td class="mv">${h(fmtMonth(iDate))}</td></tr>` : ""}
|
${iDate ? `<tr><td class="ml">${h(td("invoice-date"))}</td><td class="mv">${h(fmtDate(iDate))}</td></tr>` : ""}
|
||||||
${pCode ? `<tr><td class="ml">${h(td("project-code"))}</td><td class="mv">${h(pCode)}</td></tr>` : ""}
|
${pCode ? `<tr><td class="ml">${h(td("project-code"))}</td><td class="mv">${h(pCode)}</td></tr>` : ""}
|
||||||
|
${iCur ? `<tr><td class="ml">${h(td("invoice-currency"))}</td><td class="mv">${h(iCur)}</td></tr>` : ""}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1021,7 +1049,7 @@ function buildPDF() {
|
||||||
const tR = (s,x,y) => doc.text(String(s??""), x, y, {align:"right"});
|
const tR = (s,x,y) => doc.text(String(s??""), x, y, {align:"right"});
|
||||||
const sp = (s,w) => doc.splitTextToSize(String(s??""), w);
|
const sp = (s,w) => doc.splitTextToSize(String(s??""), w);
|
||||||
|
|
||||||
const { dl, td, sName, sAddr, sCntry, sPh, sEm, iDate, pCode, iNo,
|
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, taxRate, taxAmt, taxLabel, paid, toPay } = gatherData();
|
||||||
|
|
||||||
|
|
@ -1054,8 +1082,9 @@ function buildPDF() {
|
||||||
// Meta table (right-aligned)
|
// Meta table (right-aligned)
|
||||||
const metaRows = [
|
const metaRows = [
|
||||||
iNo ? [td("invoice-no"), iNo] : null,
|
iNo ? [td("invoice-no"), iNo] : null,
|
||||||
iDate ? [td("invoice-date"), fmtMonth(iDate)] : null,
|
iDate ? [td("invoice-date"), fmtDate(iDate)]: null,
|
||||||
pCode ? [td("project-code"), pCode] : null,
|
pCode ? [td("project-code"), pCode] : null,
|
||||||
|
iCur ? [td("invoice-currency"), iCur] : null,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
metaRows.forEach(([lbl, val]) => {
|
metaRows.forEach(([lbl, val]) => {
|
||||||
|
|
@ -1116,13 +1145,20 @@ function buildPDF() {
|
||||||
y += TH;
|
y += TH;
|
||||||
|
|
||||||
const ROW_H = 7.5;
|
const ROW_H = 7.5;
|
||||||
const FX_H = 4.5;
|
|
||||||
|
|
||||||
rows.forEach((row, idx) => {
|
rows.forEach((row, idx) => {
|
||||||
// Calculate row height (description may wrap)
|
// Calculate row height (description and fx note may wrap)
|
||||||
const dLines = sp(row.desc, CD - 4);
|
const dLines = sp(row.desc, CD - 4);
|
||||||
const descH = Math.max(0, (dLines.length - 1) * 3.8);
|
const descH = Math.max(0, (dLines.length - 1) * 3.8);
|
||||||
const rh = ROW_H + descH + (row.fxNote ? FX_H : 0);
|
let fxH = 0;
|
||||||
|
if (row.fxNote) {
|
||||||
|
const fxStr = `${td("converted-from")} ${row.fxNote.cur}: 1 ${row.fxNote.cur} = ${(+row.fxNote.rate).toFixed(5)} ${iCur}. `
|
||||||
|
+ `${td("per-item")}: ${row.fxNote.cur} ${fmt(row.fxNote.per)}, `
|
||||||
|
+ `${td("line-total").toLowerCase()}: ${row.fxNote.cur} ${fmt(row.fxNote.foreignTot)}`;
|
||||||
|
const fxLines = sp(fxStr, CD + CP - 4);
|
||||||
|
fxH = fxLines.length * 3.5 + 1;
|
||||||
|
}
|
||||||
|
const rh = ROW_H + descH + fxH;
|
||||||
|
|
||||||
if (y + rh > PH - 30) { doc.addPage(); y = MT; }
|
if (y + rh > PH - 30) { doc.addPage(); y = MT; }
|
||||||
|
|
||||||
|
|
@ -1150,10 +1186,14 @@ function buildPDF() {
|
||||||
fb(8.5); tc(17,24,39); tR(fmt(row.tot), XR-2, yt);
|
fb(8.5); tc(17,24,39); tR(fmt(row.tot), XR-2, yt);
|
||||||
|
|
||||||
if (row.fxNote) {
|
if (row.fxNote) {
|
||||||
const fxStr = `${td("foreign-currency")}: ${row.fxNote.cur} ${fmt(row.fxNote.per)}`
|
const fxStr = `${td("converted-from")} ${row.fxNote.cur}: `
|
||||||
+ ` / ${row.fxNote.rate} × ${row.qty} = ${fmt(row.fxNote.ltot)}`;
|
+ `1 ${row.fxNote.cur} = ${(+row.fxNote.rate).toFixed(5)} ${iCur}. `
|
||||||
|
+ `${td("per-item")}: ${row.fxNote.cur} ${fmt(row.fxNote.per)}, `
|
||||||
|
+ `${td("line-total").toLowerCase()}: ${row.fxNote.cur} ${fmt(row.fxNote.foreignTot)}`;
|
||||||
fn(7); tc(107,114,128);
|
fn(7); tc(107,114,128);
|
||||||
tL(fxStr, xD+2, y + ROW_H + descH + 3.2);
|
// Split if too long for the description column
|
||||||
|
const fxLines = sp(fxStr, CD + CP - 4);
|
||||||
|
fxLines.forEach((fl, fi) => tL(fl, xD+2, y + ROW_H + descH + 3.2 + fi * 3.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
y += rh;
|
y += rh;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue