#!/usr/bin/env node /* * signet-to-ubl.js * Projects a SIGNET Canonical Data Model Invoice into a UBL 2.1 Invoice * conforming to Peppol BIS Billing 3.0 (EN 16931 compliant). * * Usage: node tools/signet-to-ubl.js [path/to/invoice.json] > out.xml * Default input: examples/invoice.json * * This is a reference projection, not a validator. It demonstrates that a * SIGNET invoice carries exactly the EN 16931 Business Terms that UBL / Peppol * BIS Billing require. Each mapping is annotated with its BT/BG reference. * * Licensed CC0 1.0 by Concert Foundation. */ const fs = require("fs"); const path = require("path"); /** * Pure transform: SIGNET canonical Invoice -> Peppol BIS Billing 3.0 UBL string. * No I/O; safe to import in Node or the browser. */ function toUBL(inv) { // --- helpers --- const esc = (s) => String(s) .replace(/&/g, "&").replace(//g, ">") .replace(/"/g, """).replace(/'/g, "'"); const date = (dt) => (dt ? String(dt).slice(0, 10) : undefined); // ISO date-time -> date (UBL) const money = (v) => (v == null ? undefined : Number(v.amount).toFixed(2)); const cur = (v) => (v && v.currency) || inv.currency; // Map a SIGNET Identifier to a UBL party scheme. // Peppol/ICD: LEI = 0199. DIDs have no ICD code; emit as plain ID (demonstration). const PARTY_SCHEME = { "gleif:lei": "0199" }; function companyId(idObj) { const scheme = PARTY_SCHEME[idObj.scheme]; return scheme ? `${esc(idObj.id)}` : `${esc(idObj.id)}`; } const L = []; // line buffer const w = (s) => L.push(s); // --- document header --- w(``); w(``); // Peppol BIS Billing 3.0 customization + profile w(` urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0`); w(` urn:fdc:peppol.eu:2017:poacc:billing:01:1.0`); w(` ${esc(inv.id.id)}`); // BT-1 w(` ${date(inv.issueDate)}`); // BT-2 if (inv.paymentDueDate) w(` ${date(inv.paymentDueDate)}`); // BT-9 w(` ${esc(inv.invoiceTypeCode || "380")}`); // BT-3 w(` ${esc(inv.currency)}`); // BT-5 // references if (inv.order) { // BT-13 w(` ${esc(inv.order.id)}`); } if (inv.contract) { // BT-12 w(` ${esc(inv.contract.id)}`); } // seller (BG-4) w(` `); w(` ${companyId(inv.seller)}`); // BT-27/BT-30 w(` `); // buyer (BG-7) w(` `); w(` ${companyId(inv.buyer)}`); // BT-44/BT-47 w(` `); // payment terms (BT-20) if (inv.paymentTerms) { w(` ${esc(inv.paymentTerms)}`); } // tax total (BG-22 / BT-110) with VAT breakdown (BG-23) const taxTotal = money(inv.taxTotal); w(` `); w(` ${taxTotal}`); // BT-110 for (const vb of (inv.vatBreakdown || [])) { w(` `); w(` ${money(vb.taxableAmount)}`); // BT-116 w(` ${money(vb.taxAmount)}`); // BT-117 w(` `); w(` ${esc(vb.categoryCode)}`); // BT-118 w(` ${vb.rate}`); // BT-119 w(` VAT`); w(` `); w(` `); } w(` `); // legal monetary total (BG-22) w(` `); w(` ${money(inv.lineExtensionTotal)}`); // BT-106 w(` ${money(inv.taxExclusiveTotal)}`); // BT-109 w(` ${money(inv.taxInclusiveTotal)}`); // BT-112 w(` ${money(inv.payableAmount)}`); // BT-115 w(` `); // invoice lines (BG-25) for (const ln of inv.lines) { w(` `); w(` ${esc(ln.id)}`); // BT-126 if (ln.note) w(` ${esc(ln.note)}`); // BT-127 const uom = ln.unitOfMeasure || "C62"; w(` ${ln.quantity}`); // BT-129/130 w(` ${money(ln.netAmount)}`); // BT-131 w(` `); w(` ${esc(ln.itemName)}`); // BT-153 if (ln.vatCategoryCode) { w(` `); w(` ${esc(ln.vatCategoryCode)}`); // BT-151 if (ln.vatRate != null) w(` ${ln.vatRate}`); w(` VAT`); w(` `); } if (ln.classification) { // BT-158 const listId = (ln.classification.scheme || "").toUpperCase(); w(` `); w(` ${esc(ln.classification.id)}`); w(` `); } w(` `); if (ln.itemNetPrice) { w(` ${money(ln.itemNetPrice)}`); // BT-146 } w(` `); } w(``); return L.join("\n") + "\n"; } // --- CLI wrapper --- if (require.main === module) { const inPath = process.argv[2] || path.join(__dirname, "..", "examples", "invoice.json"); const inv = JSON.parse(fs.readFileSync(inPath, "utf8")); process.stdout.write(toUBL(inv)); } module.exports = { toUBL };