"\u003c!DOCTYPE html>\n\u003chtml lang=\"fi\">\n\u003chead>\n \u003cmeta charset=\"utf-8\">\n \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n \u003ctitle>EcoBooks — Invoice Generator\u003c/title>\n \u003cstyle>\n/* ── Paged media ─────────────────────────────────────────────────────────── */\n@page{margin:0;size:A4}\n\n/* ── Reset \u0026 base ─────────────────────────────────────────────────────────── */\n*{box-sizing:border-box;margin:0;padding:0}\nbody{font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif;\n font-size:13px;background:#f5f7fa;color:#222;padding:0 0 40px}\n\n/* ── Page header ─────────────────────────────────────────────────────────── */\n.page-header{background:#1a3a5c;color:#fff;padding:12px 18px;display:flex;\n align-items:center;gap:10px;position:sticky;top:0;z-index:100}\n.page-header h1{font-size:1rem;font-weight:700}\n.page-header .back-btn{background:rgba(255,255,255,0.15);border:none;color:#fff;\n padding:6px 12px;border-radius:5px;cursor:pointer;font-size:0.85rem;margin-left:auto}\n.page-header .back-btn:hover{background:rgba(255,255,255,0.25)}\n\n/* ── Form wrapper ─────────────────────────────────────────────────────────── */\n#formPanel{max-width:860px;margin:16px auto;padding:0 14px}\n\n/* ── Section cards ───────────────────────────────────────────────────────── */\n.section{background:#fff;border-radius:9px;padding:14px 16px;margin-bottom:12px;\n box-shadow:0 1px 3px rgba(0,0,0,0.07)}\n.section-title{font-weight:700;font-size:0.85rem;text-transform:uppercase;\n letter-spacing:0.06em;color:#1a3a5c;margin-bottom:10px;padding-bottom:6px;\n border-bottom:1.5px solid #e8eef5}\n\n/* ── Grid helpers ────────────────────────────────────────────────────────── */\n.row2{display:grid;grid-template-columns:1fr 1fr;gap:10px}\n.row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px}\n.row4{display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:10px}\n\n/* ── Form fields ─────────────────────────────────────────────────────────── */\nlabel{display:block;font-size:11px;font-weight:600;color:#666;margin-bottom:3px;margin-top:8px}\nlabel:first-child{margin-top:0}\ninput[type=\"text\"],input[type=\"number\"],select{\n width:100%;padding:7px 9px;border:1.5px solid #dde3ea;border-radius:5px;\n font-size:12.5px;background:#fff;color:#222;font-family:inherit}\ninput:focus,select:focus{outline:none;border-color:#1a3a5c}\ninput[readonly]{background:#f8f8f8;color:#555}\n\n/* ── Buttons ─────────────────────────────────────────────────────────────── */\n.btn-primary{background:#1a3a5c;color:#fff;border:none;border-radius:6px;\n padding:9px 20px;font-size:0.9rem;font-weight:700;cursor:pointer}\n.btn-primary:hover{background:#14304f}\n.btn-secondary{background:#f0f4f8;color:#1a3a5c;border:1.5px solid #c5d3e0;\n border-radius:6px;padding:7px 14px;font-size:0.85rem;font-weight:600;cursor:pointer}\n.btn-secondary:hover{background:#e4edf5}\n.btn-remove{background:none;border:none;cursor:pointer;color:#c00;font-size:1rem;\n padding:2px 5px;line-height:1}\n\n/* ── Recipient profile row ───────────────────────────────────────────────── */\n.profile-row{display:flex;gap:8px;align-items:flex-end;margin-bottom:8px}\n.profile-row select{flex:1}\n.profile-row button{white-space:nowrap;padding:7px 10px;min-width:36px}\n\n/* ── Line items table ────────────────────────────────────────────────────── */\n#lineItemsTable{width:100%;border-collapse:collapse;font-size:12px}\n#lineItemsTable th{background:#f0f4f8;padding:5px 7px;text-align:left;\n font-weight:600;border-bottom:1.5px solid #dde3ea;font-size:11px}\n#lineItemsTable td{padding:3px 4px;border-bottom:1px solid #f3f3f3;vertical-align:middle}\n#lineItemsTable input,#lineItemsTable select{padding:5px 6px;font-size:12px}\n\n/* ── Action strip ────────────────────────────────────────────────────────── */\n.action-strip{display:flex;gap:10px;justify-content:flex-end;align-items:flex-end;padding-top:4px}\n.action-strip .lang-pick{margin-right:auto}\n.action-strip .lang-pick select{width:auto;min-width:190px}\n\n/* ── Print / preview panel ───────────────────────────────────────────────── */\n#previewPanel{display:none;margin:16px auto;padding:0 14px;overflow-x:auto}\n.preview-btns{display:flex;gap:10px;margin-bottom:12px;max-width:860px;\n margin-left:auto;margin-right:auto;align-items:center}\n.btn-back{background:#f0f4f8;color:#1a3a5c;border:1.5px solid #c5d3e0;border-radius:6px;\n padding:8px 16px;font-size:0.9rem;font-weight:600;cursor:pointer}\n.btn-print{background:#1a5c2a;color:#fff;border:none;border-radius:6px;\n padding:8px 18px;font-size:0.9rem;font-weight:700;cursor:pointer}\n.btn-back:hover{background:#e4edf5}\n.btn-print:hover{background:#154820}\n.preview-btns .spacer{flex:1}\n.preview-note{max-width:860px;margin:0 auto 12px;padding:8px 12px;border-radius:5px;\n background:#fff4d6;border:1px solid #e0b400;color:#5a4300;font-size:12px;font-weight:600}\n\n/* ═══════════════════════════════════════════════════════════════════════════\n INVOICE — the printed document.\n Ported from invoice_redesign/template.html (the layout Hassan approved\n against two real samples), with three deliberate departures, each noted at\n the point it occurs:\n · page 2 is NOT fixed-height (the template's height:297mm + overflow:hidden\n would silently CLIP a breakdown longer than one page — the same class of\n bug this redesign exists to remove);\n · the header grows with the key-value table instead of overlapping it;\n · the giro labels and the payment-transmission disclaimer stay the\n statutory FI/SV pairs in BOTH languages.\n ═══════════════════════════════════════════════════════════════════════════ */\n#invoiceOutput{font-family:Helvetica,Arial,\"Liberation Sans\",sans-serif;\n color:#000;font-size:9pt;-webkit-print-color-adjust:exact;print-color-adjust:exact}\n#invoiceOutput .iv-page{width:210mm;padding:0 10mm;position:relative;\n background:#fff;margin:0 auto 14px;box-shadow:0 1px 6px rgba(0,0,0,0.12)}\n/* ★ PAGE 1 ONLY is a fixed-height box. That is the whole mechanism: the footer\n is pinned to the bottom of a box whose height does not depend on row count,\n so the giro form and barcode CANNOT be pushed off the page. */\n#invoiceOutput .iv-p1{height:297mm;overflow:hidden}\n/* ★ PAGE 2+ deliberately flows. A long breakdown paginates onto further sheets\n rather than being clipped. */\n#invoiceOutput .iv-p2{min-height:297mm;padding:17mm 10mm 16mm}\n\n#invoiceOutput .iv-flow{transform-origin:top center}\n\n/* ── header ─────────────────────────────────────────────────────────────── */\n#invoiceOutput .head{position:relative;min-height:78mm}\n#invoiceOutput .from{position:absolute;left:11mm;top:17mm;line-height:1.5}\n#invoiceOutput .from .nm{font-weight:bold}\n#invoiceOutput .to{position:absolute;left:11mm;top:40mm;font-weight:bold;line-height:1.5}\n#invoiceOutput h1.inv-title{position:absolute;left:108mm;top:15mm;margin:0;\n font-size:19pt;letter-spacing:.2px;font-weight:bold}\n#invoiceOutput .pg{position:absolute;right:0;top:17mm;font-size:8.5pt;color:#555}\n/* ★ in FLOW, not absolute: the head then grows when optional rows (delivery\n date, delivery terms, …) are present, instead of the table silently\n overlapping the line items below it. */\n#invoiceOutput .kvwrap{margin-left:108mm;width:82mm;padding-top:26mm}\n#invoiceOutput table.kv{width:100%;border-collapse:collapse}\n#invoiceOutput table.kv th{text-align:left;font-weight:bold;padding:1.45mm 0;\n font-size:9pt;white-space:nowrap}\n#invoiceOutput table.kv td{text-align:left;padding:1.45mm 0;padding-left:8mm;\n font-size:9pt;white-space:nowrap}\n\n#invoiceOutput .period{margin:0 0 7mm 1mm;font-size:9pt}\n\n/* ── line table ─────────────────────────────────────────────────────────── */\n#invoiceOutput table.lines{width:100%;border-collapse:collapse;font-size:9pt}\n#invoiceOutput table.lines thead th{background:#e9e9e9;font-weight:bold;\n padding:2.4mm 2mm;text-align:left}\n#invoiceOutput table.lines thead th.r{text-align:right}\n#invoiceOutput table.lines thead th.c{text-align:center}\n#invoiceOutput table.lines td{padding:1.75mm 2mm}\n#invoiceOutput table.lines td.r{text-align:right}\n#invoiceOutput table.lines td.c{text-align:center}\n#invoiceOutput table.lines tr{page-break-inside:avoid;break-inside:avoid}\n/* repeat the breakdown's header on every printed sheet it spills onto */\n#invoiceOutput table.lines thead{display:table-header-group}\n\n/* ── totals block ───────────────────────────────────────────────────────── */\n#invoiceOutput .totwrap{display:flex;align-items:flex-start;margin-top:6mm}\n#invoiceOutput .totleft{flex:1;font-style:italic;font-size:9pt;padding-left:1mm}\n#invoiceOutput table.tot{width:62mm;border-collapse:collapse;font-size:8.5pt}\n#invoiceOutput table.tot td{border-bottom:.6pt solid #b8b8b8;padding:1.7mm 0 1.5mm}\n#invoiceOutput table.tot td.v{text-align:right}\n#invoiceOutput table.tot tr.due td{border-bottom:none;font-size:12pt;\n font-weight:bold;padding-top:3.2mm}\n\n/* ── footer block — pinned; its position does not depend on row count ───── */\n#invoiceOutput .foot{position:absolute;left:10mm;right:10mm;bottom:10mm}\n#invoiceOutput .rule{border-top:.8pt solid #000}\n#invoiceOutput .info{display:flex;padding:2.2mm 0 3.2mm;font-size:7pt}\n#invoiceOutput .info .col{width:97mm}\n#invoiceOutput .info .col.b{width:auto;flex:1;padding-left:10mm}\n#invoiceOutput .info .r{display:flex;padding:.55mm 0}\n#invoiceOutput .info .r .l{width:37mm;color:#333}\n#invoiceOutput .info .r .v{flex:1}\n#invoiceOutput .cut{border-top:1px dashed #000}\n\n/* ── Finnish bank giro (tilisiirto) ─────────────────────────────────────── */\n#invoiceOutput table.giro{width:100%;border-collapse:collapse;font-size:8.5pt;\n table-layout:fixed}\n#invoiceOutput table.giro td{border:1px solid #000;padding:1.5mm 2mm;vertical-align:top}\n#invoiceOutput table.giro td.lab{border:none;text-align:right;\n padding:1.5mm 1.8mm 0 0;font-size:6pt;width:24mm;line-height:1.3}\n#invoiceOutput .tag{font-size:6.5pt;display:block;margin-bottom:.6mm}\n#invoiceOutput .big{font-size:10pt}\n#invoiceOutput .cellh{height:17mm;line-height:1.55}\n#invoiceOutput .sig{border-bottom:1px solid #000;margin-top:7.5mm}\n\n#invoiceOutput .bcwrap{display:flex;align-items:flex-end;padding-top:2.5mm;gap:5mm}\n#invoiceOutput .bcwrap .bc{flex:0 0 112mm}\n#invoiceOutput .bcwrap .bc svg{display:block;margin:0}\n#invoiceOutput .bcdigits{font-size:7.5pt;text-align:center;letter-spacing:.15px;\n padding-bottom:1mm}\n#invoiceOutput .terms{flex:1;font-size:6.5pt;line-height:1.38;padding-bottom:1.5mm}\n\n/* ── page 2 ─────────────────────────────────────────────────────────────── */\n#invoiceOutput h2.bd{font-size:13pt;margin:0 0 1mm 1mm}\n#invoiceOutput .sub{margin:0 0 6mm 1mm;font-size:8.5pt;color:#333}\n#invoiceOutput table.p2tot{margin-top:6mm;width:62mm;margin-left:auto;\n border-collapse:collapse;font-size:8.5pt}\n#invoiceOutput table.p2tot td{border-bottom:.6pt solid #b8b8b8;padding:1.7mm 0 1.5mm}\n#invoiceOutput table.p2tot td.v{text-align:right}\n#invoiceOutput table.p2tot tr.due td{border-bottom:none;font-size:11pt;\n font-weight:bold;padding-top:3mm}\n\n/* ── Print ──────────────────────────────────────────────────────────────── */\n@media print{\n @page{margin:0;size:A4}\n html,body{height:auto}\n body{background:#fff;padding:0;margin:0}\n .page-header,#formPanel,.preview-btns,.preview-note,.no-print{display:none!important}\n #previewPanel{display:block!important;padding:0!important;margin:0!important;\n max-width:none!important;background:#fff!important}\n #invoiceOutput .iv-page{width:100%;margin:0;box-shadow:none;\n page-break-after:always;break-after:page}\n #invoiceOutput .iv-page:last-child{page-break-after:auto;break-after:auto}\n /* 0.2mm of slack so sub-pixel rounding on a 297.00mm box can never spill a\n blank trailing sheet. Invisible: it moves the pinned footer by 0.2mm. */\n #invoiceOutput .iv-p1{height:296.8mm}\n /* ★ min-height:297mm only exists to make page 2 LOOK like a sheet in the\n on-screen preview. In print it made a breakdown that happened to be\n exactly one sheet tall spill a blank third sheet. Caught by counting\n pages in the produced PDF, not by looking at the preview. */\n #invoiceOutput .iv-p2{min-height:0}\n}\n \u003c/style>\n\u003c/head>\n\u003cbody>\n\n\u003c!-- ═══════════════════════ PAGE HEADER ══════════════════════════════════════ -->\n\u003cdiv class=\"page-header\">\n \u003ch1>📄 EcoBooks — Create a new invoice\u003c/h1>\n \u003cbutton class=\"back-btn\" id=\"pageBackBtn\" onclick=\"window.history.back()\">← Back\u003c/button>\n \u003cscript>/* s377: inside Scribe 2.0 the page is embedded and has no history of its own -- the\n composer's menu is the way back, so the dead Back button is not shown there. */\n try { if (window.parent \u0026\u0026 window.parent !== window) document.getElementById(\"pageBackBtn\").style.display = \"none\"; } catch (e) {}\u003c/script>\n\u003c/div>\n\n\u003c!-- ═══════════════════════ FORM ══════════════════════════════════════════════ -->\n\u003cdiv id=\"formPanel\">\n\n \u003c!-- Sender -->\n \u003cdiv class=\"section\">\n \u003cdiv class=\"section-title\">Sender details \u003cspan style=\"font-weight:normal;color:#888;font-size:10px\">(saved automatically, for this client, on every computer)\u003c/span> \u003cspan id=\"senderSaved\" style=\"font-weight:normal;font-size:10px;color:#2e7d32\">\u003c/span>\u003c/div>\n \u003cdiv class=\"row2\">\n \u003cdiv>\u003clabel>Company name\u003c/label>\u003cinput type=\"text\" id=\"sCompanyName\" placeholder=\"Company Ltd\">\u003c/div>\n \u003cdiv>\u003clabel>Business ID\u003c/label>\u003cinput type=\"text\" id=\"sYtunnus\" placeholder=\"1234567-8\">\u003c/div>\n \u003c/div>\n \u003cdiv>\u003clabel>Street address\u003c/label>\u003cinput type=\"text\" id=\"sAddress\" placeholder=\"Example street 1\">\u003c/div>\n \u003cdiv class=\"row2\">\n \u003cdiv>\u003clabel>Postal code and city\u003c/label>\u003cinput type=\"text\" id=\"sPostalCity\" placeholder=\"00100 Helsinki\">\u003c/div>\n \u003cdiv>\u003clabel>Domicile\u003c/label>\u003cinput type=\"text\" id=\"sDomicile\" placeholder=\"Helsinki\">\u003c/div>\n \u003c/div>\n \u003cdiv class=\"row2\">\n \u003cdiv>\u003clabel>IBAN\u003c/label>\u003cinput type=\"text\" id=\"sIban\" placeholder=\"FI97 1564 2972 8532 89\">\u003c/div>\n \u003cdiv>\u003clabel>BIC\u003c/label>\u003cinput type=\"text\" id=\"sBic\" placeholder=\"NDEAFIHH\">\u003c/div>\n \u003c/div>\n \u003cdiv class=\"row2\">\n \u003cdiv>\u003clabel>Bank\u003c/label>\u003cinput type=\"text\" id=\"sBankName\" placeholder=\"Nordea\">\u003c/div>\n \u003cdiv>\u003clabel>Phone\u003c/label>\u003cinput type=\"text\" id=\"sPhone\" placeholder=\"+358 40 123 4567\">\u003c/div>\n \u003c/div>\n \u003cdiv class=\"row2\">\n \u003cdiv>\u003clabel>E-mail\u003c/label>\u003cinput type=\"text\" id=\"sEmail\" placeholder=\"email@example.com\">\u003c/div>\n \u003cdiv>\u003clabel>Website (optional)\u003c/label>\u003cinput type=\"text\" id=\"sWeb\" placeholder=\"www.example.com\">\u003c/div>\n \u003c/div>\n \u003c/div>\n\n \u003c!-- Invoice metadata -->\n \u003cdiv class=\"section\">\n \u003cdiv class=\"section-title\">Invoice details\u003c/div>\n \u003cdiv class=\"row3\">\n \u003cdiv>\u003clabel>Invoice number\u003c/label>\u003cinput type=\"number\" id=\"invoiceNumber\" min=\"1\" value=\"1\" readonly style=\"background:#f8f8f8;color:#555\" title=\"Scribe 2.0 gives the next number when the invoice is issued\">\u003c/div>\n \u003cdiv>\u003clabel>Date (DD.MM.YYYY)\u003c/label>\u003cinput type=\"text\" id=\"invoiceDate\" placeholder=\"31.3.2026\">\u003c/div>\n \u003cdiv>\n \u003clabel>Payment terms\u003c/label>\n \u003cselect id=\"paymentTerms\">\n \u003coption value=\"7\">7 days net\u003c/option>\n \u003coption value=\"14\" selected>14 days net\u003c/option>\n \u003coption value=\"30\">30 days net\u003c/option>\n \u003c/select>\n \u003c/div>\n \u003c/div>\n \u003cdiv class=\"row2\">\n \u003cdiv>\u003clabel>Due date (DD.MM.YYYY)\u003c/label>\u003cinput type=\"text\" id=\"dueDate\" placeholder=\"14.4.2026\">\u003c/div>\n \u003cdiv>\u003clabel>Reference number (auto)\u003c/label>\u003cinput type=\"text\" id=\"viitenumero\" readonly style=\"background:#f8f8f8;color:#555\">\u003c/div>\n \u003c/div>\n \u003cdiv class=\"row4\">\n \u003cdiv>\u003clabel>Late payment interest %\u003c/label>\u003cinput type=\"text\" id=\"lateInterest\" value=\"8\">\u003c/div>\n \u003cdiv>\u003clabel>Delivery date\u003c/label>\u003cinput type=\"text\" id=\"deliveryDate\" placeholder=\"8.4.2026\">\u003c/div>\n \u003cdiv>\u003clabel>Period for complaints (days)\u003c/label>\u003cinput type=\"text\" id=\"reminderDays\" placeholder=\"8 days\">\u003c/div>\n \u003cdiv>\u003clabel>Service period (optional)\u003c/label>\u003cinput type=\"text\" id=\"servicePeriod\" placeholder=\"01.03.2026-31.03.2026\">\u003c/div>\n \u003c/div>\n \u003cdiv class=\"row2\">\n \u003cdiv>\u003clabel>Delivery terms\u003c/label>\u003cinput type=\"text\" id=\"deliveryTerms\" placeholder=\"Ex works\">\u003c/div>\n \u003cdiv>\u003clabel>Delivery method\u003c/label>\u003cinput type=\"text\" id=\"deliveryMethod\" placeholder=\"Delivered\">\u003c/div>\n \u003c/div>\n \u003c/div>\n\n \u003c!-- Recipient -->\n \u003cdiv class=\"section\">\n \u003cdiv class=\"section-title\">Recipient\u003c/div>\n\n \u003c!-- Saved profiles dropdown (hidden until profiles exist) -->\n \u003cdiv id=\"recipient-profile-row\" class=\"profile-row\" style=\"display:none\">\n \u003cdiv style=\"flex:1\">\n \u003clabel>Saved recipients\u003c/label>\n \u003cselect id=\"recipientProfile\" onchange=\"loadRecipientProfile()\">\n \u003coption value=\"\">— Select a saved recipient —\u003c/option>\n \u003c/select>\n \u003c/div>\n \u003cbutton type=\"button\" class=\"btn-secondary\" onclick=\"deleteRecipientProfile()\" title=\"Delete selected\">✕\u003c/button>\n \u003c/div>\n\n \u003cdiv class=\"row2\">\n \u003cdiv>\u003clabel>Company name / Name\u003c/label>\u003cinput type=\"text\" id=\"recipientName\" placeholder=\"Indulge Oy\">\u003c/div>\n \u003cdiv>\u003clabel>Business ID\u003c/label>\u003cinput type=\"text\" id=\"recipientYtunnus\" placeholder=\"1234567-8\">\u003c/div>\n \u003c/div>\n \u003cdiv>\u003clabel>Street address\u003c/label>\u003cinput type=\"text\" id=\"recipientAddress\" placeholder=\"Example street 1\">\u003c/div>\n \u003cdiv class=\"row2\">\n \u003cdiv>\u003clabel>Postal code and city\u003c/label>\u003cinput type=\"text\" id=\"recipientPostal\" placeholder=\"00100 Helsinki\">\u003c/div>\n \u003cdiv>\u003clabel>Customer number (optional)\u003c/label>\u003cinput type=\"text\" id=\"recipientCustNum\">\u003c/div>\n \u003c/div>\n \u003cdiv style=\"margin-top:8px\">\n \u003clabel style=\"display:flex;align-items:center;gap:6px;cursor:pointer;font-weight:normal\">\n \u003cinput type=\"checkbox\" id=\"saveRecipient\" checked>\n Save recipient as a profile (auto-fill next time)\n \u003c/label>\n \u003c/div>\n \u003c/div>\n\n \u003c!-- Line items -->\n \u003cdiv class=\"section\">\n \u003cdiv class=\"section-title\">Line items\u003c/div>\n \u003ctable id=\"lineItemsTable\">\n \u003cthead>\n \u003ctr>\n \u003cth style=\"width:7%\">Code\u003c/th>\n \u003cth style=\"width:24%\">Description\u003c/th>\n \u003cth style=\"width:11%\" title=\"Leave blank for no grouping. Lines sharing a Type, VAT rate and Unit are summarised into one line on page 1 of the PDF, with the full breakdown on page 2.\">Type\u003c/th>\n \u003cth style=\"width:6%\">Qty\u003c/th>\n \u003cth style=\"width:6%\">Unit\u003c/th>\n \u003cth style=\"width:11%\">Unit price €\u003c/th>\n \u003cth style=\"width:9%\">VAT %\u003c/th>\n \u003cth style=\"width:11%\">Net €\u003c/th>\n \u003cth style=\"width:11%\">Gross €\u003c/th>\n \u003cth style=\"width:4%\">\u003c/th>\n \u003c/tr>\n \u003c/thead>\n \u003ctbody id=\"lineItemsBody\">\u003c/tbody>\n \u003c/table>\n \u003cbutton type=\"button\" class=\"btn-secondary\" onclick=\"addLineItem()\" style=\"margin-top:6px;font-size:11px\">+ Add row\u003c/button>\n \u003c/div>\n\n \u003c!-- Actions -->\n \u003cdiv class=\"action-strip\">\n \u003cdiv class=\"lang-pick\">\n \u003clabel for=\"pdfLang\">Language of the printed invoice\u003c/label>\n \u003cselect id=\"pdfLang\">\n \u003coption value=\"fi\" selected>Suomi (Finnish)\u003c/option>\n \u003coption value=\"en\">English\u003c/option>\n \u003c/select>\n \u003c/div>\n \u003cbutton type=\"button\" class=\"btn-secondary\" onclick=\"window.history.back()\">Cancel\u003c/button>\n \u003cbutton type=\"button\" class=\"btn-primary\" onclick=\"generateInvoice()\">Create invoice →\u003c/button>\n \u003c/div>\n\u003c/div>\n\n\u003c!-- ═══════════════════════ PREVIEW ═══════════════════════════════════════════ -->\n\u003cdiv id=\"previewPanel\">\n \u003cdiv class=\"preview-btns no-print\">\n \u003cbutton class=\"btn-back\" onclick=\"backToForm()\">← Edit\u003c/button>\n \u003cbutton class=\"btn-print\" onclick=\"window.print()\">🖨 Print / Save PDF\u003c/button>\n \u003cspan class=\"spacer\">\u003c/span>\n \u003cspan style=\"font-size:12px;color:#555;font-weight:600\">Language:\u003c/span>\n \u003cbutton class=\"btn-back\" id=\"langFi\" onclick=\"switchLang('fi')\">Suomi\u003c/button>\n \u003cbutton class=\"btn-back\" id=\"langEn\" onclick=\"switchLang('en')\">English\u003c/button>\n \u003c/div>\n \u003cdiv id=\"previewNotes\">\u003c/div>\n \u003cdiv id=\"invoiceOutput\">\u003c/div>\n\u003c/div>\n\n\u003cscript>\n// =============================================================================\n// SHEET ID — injected by Apps Script doGet template\n// =============================================================================\nvar PE_CLIENT_ID = \"__SCRIBE_INVOICE_CLIENT__\"; // Scribe client id; no sheet behind this tool\n\n// =============================================================================\n// ★ LAYOUT CONSTANTS — measured, not chosen\n// =============================================================================\n// ★★ THE CAPACITY OF PAGE 1 IS MEASURED AT RENDER TIME, NOT ASSUMED.\n//\n// invoice_redesign/calibrate.py measured the redesign's page 1 offline:\n// 3 rows -> 22.3mm clear 5 -> 7.9mm 6 -> 0.7mm ★ COLLIDES\n// and concluded MAX_P1_LINES = 5. Re-measured in a real browser against THIS\n// footer (which keeps the statutory FI/SV disclaimer the redesign dropped, so\n// the footer is ~12mm taller):\n// 1 -> 24.4mm 2 -> 17.2mm 3 -> 10.0mm 4 -> 2.8mm 5 -> ★ -4.4mm\n//\n// ★★★ AND THE REDESIGN'S CLOSURE ARGUMENT IS FALSE. build.py argues the lump\n// fallback \"can never itself overflow: Finland has four VAT rates against a\n// capacity of five.\" It counted the lump LINES and not the TOTALS ROWS,\n// which grow with the number of rates at the same time. Measured with four\n// VAT bands present:\n// 1 line -> 24.4mm 2 -> 10.5mm 3 -> ★ -3.3mm 4 -> ★ -17.1mm\n// A four-rate invoice's own four-line fallback misses by 17mm. Each extra\n// rate costs ~13.7mm (one lump line AND one totals row) against ~7.2mm for\n// an ordinary line. The guarantee cannot rest on counting lines.\n//\n// So the guarantee here is different, and it does not depend on any constant:\n// 1. render the per-line layout;\n// 2. MEASURE the clear air above the pinned footer;\n// 3. if it is short, fall back to one lump line per VAT rate and measure again;\n// 4. if it is STILL short, scale page 1's upper block only — the footer stays\n// pinned and full size — and say so on screen.\n// Page 1 therefore cannot be truncated regardless of row count, rate count or\n// how many optional header fields the bookkeeper filled in.\n//\n// MAX_P1_LINES is now only a render-cost cutoff: beyond it, go straight to the\n// lump layout rather than building a 500-row table just to discard it.\nvar MAX_P1_LINES = 12;\n// 2.5mm of clear air. calibrate.py's 0.7mm was already \"collides\"; anything\n// above ~2mm demonstrably does not overlap. An earlier 5mm threshold demoted\n// the approved 3-line Mahendra sample, which has 3.3mm of real clear air — a\n// guard that fires on a layout Hassan had already signed off is broken, not safe.\nvar MIN_CLEAR_MM = 2.5; // required clear air above the footer\nvar PX_PER_MM = 96 / 25.4; // CSS px per mm at 96dpi\nvar MIN_CLEAR_PX = MIN_CLEAR_MM * PX_PER_MM;\n\n// ★ VAT ROUNDING BASIS — [LAW AVL 209 e §]\n// vero.fi, \"VAT invoice requirements\", section 5.1.10:\n// \"The VAT payable must be expressed with two decimals. Usual arithmetical\n// rules apply for the rounding of uneven values... Rounding should only\n// concern the final amount of the invoice.\"\n// https://www.vero.fi/en/detailed-guidance/guidance/48090/vat-invoice-requirements/\n//\n// So VAT is computed on the NET SUBTOTAL PER RATE and rounded ONCE per rate —\n// never as the sum of per-line rounded VAT amounts. Every line's net is itself\n// rounded to the cent at source (a line on an invoice is a cent amount), so\n// line totals, per-rate subtotals and the grand total reconcile exactly at\n// all three levels with no residue.\n\n// =============================================================================\n// LANGUAGE\n// =============================================================================\n// ★ The giro (tilisiirto) labels and the payment-transmission disclaimer are\n// NOT translated. They are the statutory Finnish/Swedish pairs of the standard\n// Finnish bank transfer form and stay identical in both languages —\n// [STATED Hassan, 2 Sep 2026]. invoice_redesign/build.py's EN dictionary\n// translates them; that is wrong and this file deliberately departs from it.\nvar L_FI = {\n INVOICE:\"LASKU\", INVOICE_N:\"Lasku\", DESC:\"Nimike\", DATE:\"Päivämäärä\",\n QTY:\"Määrä\", UNIT:\"Yks.\", PRICE:\"À-hinta\", VATPC:\"ALV %\", TOTAL:\"Yhteensä\",\n NET:\"Veroton\", TYPE:\"Laji\", BREAKDOWN:\"Erittely\",\n EXVAT:\"Veroton yhteensä\", VATTOT:\"ALV yhteensä\", DUEAMT:\"Maksettava yhteensä\",\n COMPANY:\"Yritys\", ADDRESS:\"Osoite\", CONUM:\"Y-tunnus\", TEL:\"Puh.\",\n EMAIL:\"Sähköposti\", WWW:\"www\",\n LUMP:\"Laskutetut palvelut\",\n SEEP2:\"Täydellinen erittely ({n} riviä) sivulla 2.\",\n PAGE:\"Sivu {a}/{b}\",\n KV_NO:\"Laskun numero:\", KV_DATE:\"Laskun päiväys:\", KV_TERMS:\"Maksuehdot:\",\n KV_DUE:\"Eräpäivä:\", KV_INT:\"Viivästyskorko:\", KV_REF:\"Viitenumero:\",\n KV_CUST:\"Asiakasnumero:\", KV_CUSTYT:\"Asiakkaan Y-tunnus:\",\n KV_DELDATE:\"Toimituspäivä:\", KV_REMIND:\"Huomautusaika:\",\n KV_DELTERMS:\"Toimitusehto:\", KV_DELMETH:\"Toimitustapa:\",\n DAYS:\"{n} päivää netto\",\n NOITEMS:\"Lisää vähintään yksi tuoterivi (nimike tai hinta).\",\n ERR:\"Virhe laskun luonnissa: \"\n};\nvar L_EN = {\n INVOICE:\"INVOICE\", INVOICE_N:\"Invoice\", DESC:\"Description\", DATE:\"Date\",\n QTY:\"Qty\", UNIT:\"Unit\", PRICE:\"Unit price\", VATPC:\"VAT %\", TOTAL:\"Total\",\n NET:\"Net\", TYPE:\"Type\", BREAKDOWN:\"Breakdown\",\n EXVAT:\"Total excl. VAT\", VATTOT:\"VAT total\", DUEAMT:\"Total amount due\",\n COMPANY:\"Company\", ADDRESS:\"Address\", CONUM:\"Company number\", TEL:\"Tel.\",\n EMAIL:\"E-mail\", WWW:\"www\",\n LUMP:\"Invoiced services\",\n SEEP2:\"Full line-by-line breakdown ({n} rows) on page 2.\",\n PAGE:\"Page {a}/{b}\",\n KV_NO:\"Invoice number:\", KV_DATE:\"Invoice date:\", KV_TERMS:\"Payment terms:\",\n KV_DUE:\"Due date:\", KV_INT:\"Late payment interest:\",\n KV_REF:\"Viitenumero (Finland):\",\n KV_CUST:\"Customer number:\", KV_CUSTYT:\"Customer business ID:\",\n KV_DELDATE:\"Delivery date:\", KV_REMIND:\"Period for complaints:\",\n KV_DELTERMS:\"Delivery terms:\", KV_DELMETH:\"Delivery method:\",\n DAYS:\"{n} days net\",\n NOITEMS:\"Add at least one line item (description or price).\",\n ERR:\"Could not create the invoice: \"\n};\n// Statutory, identical in both languages.\nvar GIRO = {\n ACCT:\"Saajan\u003cbr>tilinumero\u003cbr>Mottagarens\u003cbr>kontonummer\",\n BIC:\"BIC\",\n RECIP:\"Saaja\u003cbr>Mottagare\",\n PAYER:\"Maksajan nimi\u003cbr>ja osoite\u003cbr>Betalarens namn\u003cbr>och adress\",\n SIG:\"Allekirjoitus\u003cbr>Underskrift\",\n FROM:\"Tililtä nro\u003cbr>Från konto nr\",\n REF:\"Viitenumero\u003cbr>Ref. nr\",\n DUE:\"Eräpäivä\u003cbr>Förf.dag\",\n TOT:\"Euro\",\n VERT:\"TILISIIRTO GIRERING\",\n TERMS:\"Maksu välitetään saajalle vain Suomessa Kotimaan maksujenvälityksen \" +\n \"yleisten ehtojen mukaisesti ja vain maksajan ilmoittaman tilinumeron \" +\n \"perusteella.\u003cbr>\u003cbr>\" +\n \"Betalningen förmedlas till mottagare endast i Finland enligt Allmänna \" +\n \"villkor för inrikes betalningsförmedling och endast till det \" +\n \"kontonummer betalaren angivit.\u003cbr>\u003cbr>\u003cstrong>PANKKI BANKEN\u003c/strong>\"\n};\nfunction dict(lang){ return lang === \"en\" ? L_EN : L_FI; }\nfunction fill(s, o){\n return String(s).replace(/\\{(\\w+)\\}/g, function(_, k){\n return (o \u0026\u0026 o[k] != null) ? o[k] : \"\";\n });\n}\n\n// =============================================================================\n// RECIPIENT PROFILES\n// =============================================================================\nvar recipientProfiles = []; // [{name, ytunnus, address, postal, custNum}, ...]\n\nfunction populateProfileDropdown() {\n var sel = document.getElementById(\"recipientProfile\");\n while (sel.options.length > 1) sel.remove(1);\n recipientProfiles.forEach(function(p, i) {\n var opt = document.createElement(\"option\");\n opt.value = i;\n opt.textContent = p.name || (\"Profile \" + (i+1));\n sel.appendChild(opt);\n });\n document.getElementById(\"recipient-profile-row\").style.display =\n recipientProfiles.length > 0 ? \"flex\" : \"none\";\n}\n\nfunction loadRecipientProfile() {\n var sel = document.getElementById(\"recipientProfile\");\n var idx = parseInt(sel.value);\n if (isNaN(idx)) return;\n var p = recipientProfiles[idx];\n if (!p) return;\n function set(id,v){ if(v!=null) document.getElementById(id).value=v; }\n set(\"recipientName\", p.name);\n set(\"recipientYtunnus\", p.ytunnus);\n set(\"recipientAddress\", p.address);\n set(\"recipientPostal\", p.postal);\n set(\"recipientCustNum\", p.custNum);\n}\n\nfunction deleteRecipientProfile() {\n var sel = document.getElementById(\"recipientProfile\");\n var idx = parseInt(sel.value);\n if (isNaN(idx)) return;\n var name = recipientProfiles[idx] \u0026\u0026 recipientProfiles[idx].name;\n if (!confirm(\"Delete the saved recipient \\\"\" + (name||\"?\") + \"\\\"?\")) return;\n recipientProfiles.splice(idx, 1);\n populateProfileDropdown();\n document.getElementById(\"recipientProfile\").value = \"\";\n saveRecipientProfiles();\n}\n\nfunction saveRecipientProfiles() {\n google.script.run\n .withSuccessHandler(function(){})\n .withFailureHandler(function(e){ console.warn(\"saveInvoiceRecipients:\", e.message); })\n .saveInvoiceRecipients(PE_CLIENT_ID, JSON.stringify(recipientProfiles));\n}\n\nfunction getRecipientFields() {\n return {\n name: document.getElementById(\"recipientName\").value.trim(),\n ytunnus: document.getElementById(\"recipientYtunnus\").value.trim(),\n address: document.getElementById(\"recipientAddress\").value.trim(),\n postal: document.getElementById(\"recipientPostal\").value.trim(),\n custNum: document.getElementById(\"recipientCustNum\").value.trim()\n };\n}\n\n// =============================================================================\n// VIITENUMERO — Finnish domestic reference, weights 7-3-1 from the right\n// =============================================================================\nfunction generateViitenumeroRaw(n) {\n var base = \"8\" + String(n).padStart(4,\"0\");\n var weights = [7,3,1];\n var sum = 0;\n base.split(\"\").reverse().forEach(function(d,i){ sum += parseInt(d)*weights[i%3]; });\n return base + ((10-(sum%10))%10);\n}\nfunction formatViitenumero(raw) {\n var out=[]; var s=raw;\n while(s.length>3){ out.unshift(s.slice(-3)); s=s.slice(0,-3); }\n if(s) out.unshift(s);\n return out.join(\" \");\n}\nfunction getViitenumero(n){ return formatViitenumero(generateViitenumeroRaw(n)); }\n\n// =============================================================================\n// PANKKIVIIVAKOODI — virtual barcode, version 4 (54 digits)\n// =============================================================================\n// version(1) + IBAN without \"FI\"(16) + euros(6) + cents(2) + reserve \"000\"(3)\n// + reference right-aligned in 20 + due date yymmdd(6) = 54\n//\n// ★ Returns \"\" — no barcode at all — rather than a plausible-looking wrong one.\n// A wrong barcode is worse than no barcode: the payer scans it and pays the\n// wrong account or the wrong amount, and nothing on the page contradicts it.\n// The giro form is valid without a barcode; it is an optional convenience.\n// Reasons for refusing are returned so the bookkeeper is told, on screen.\nfunction buildBarcodeString(iban, amountEur, viitenro, dueFin) {\n var reasons = [];\n var ib = String(iban||\"\").replace(/\\s+/g,\"\").toUpperCase();\n if (!/^FI\\d{16}$/.test(ib)) {\n reasons.push(\"the IBAN is not a valid Finnish IBAN (FI + 16 digits)\");\n }\n var cents = Math.round(parseFloat(amountEur) * 100);\n if (!isFinite(cents) || cents \u003c 0) {\n reasons.push(\"the invoice total is not a valid amount\");\n } else if (cents > 99999999) {\n // ★ the field is 8 digits. The old code did .slice(-8), which silently\n // dropped the leading digits and printed a barcode for a smaller sum.\n reasons.push(\"the total exceeds 999 999,99 € — it does not fit the barcode format\");\n }\n var ref = String(viitenro||\"\").replace(/\\s+/g,\"\");\n if (!/^\\d{4,20}$/.test(ref)) {\n reasons.push(\"the reference number is not 4-20 digits\");\n }\n var p = String(dueFin||\"\").split(\".\");\n if (p.length !== 3 || !/^\\d{1,2}$/.test(p[0]) || !/^\\d{1,2}$/.test(p[1]) || !/^\\d{4}$/.test(p[2])) {\n reasons.push(\"the due date is missing or not DD.MM.YYYY\");\n }\n if (reasons.length) return { str:\"\", reasons:reasons };\n var yymmdd = p[2].slice(2) + pad(p[1]) + pad(p[0]);\n var s = \"4\" + ib.slice(2) + String(cents).padStart(8,\"0\") + \"000\" +\n ref.padStart(20,\"0\") + yymmdd;\n if (s.length !== 54) {\n return { str:\"\", reasons:[\"the computed barcode was \" + s.length + \" digits, not 54\"] };\n }\n return { str:s, reasons:[] };\n}\n\n// =============================================================================\n// SELF-CONTAINED CODE128 BARCODE SVG GENERATOR\n// =============================================================================\nfunction generateCode128SVG(text, barHeight) {\n barHeight = barHeight || 55;\n var T=[\n \"212222\",\"222122\",\"222221\",\"121223\",\"121322\",\"131222\",\"122213\",\"122312\",\n \"132212\",\"221213\",\"221312\",\"231212\",\"112232\",\"122132\",\"122231\",\"113222\",\n \"123122\",\"123221\",\"223121\",\"221132\",\"221231\",\"213212\",\"223112\",\"312131\",\n \"311222\",\"321122\",\"321221\",\"312212\",\"322112\",\"322211\",\"212123\",\"212321\",\n \"232121\",\"111323\",\"131123\",\"131321\",\"112313\",\"132113\",\"132311\",\"211313\",\n \"231113\",\"231311\",\"112133\",\"112331\",\"132131\",\"113123\",\"113321\",\"133121\",\n \"313121\",\"211331\",\"231131\",\"213113\",\"213311\",\"213131\",\"311123\",\"311321\",\n \"331121\",\"312113\",\"312311\",\"332111\",\"314111\",\"221411\",\"431111\",\"111224\",\n \"111422\",\"121124\",\"121421\",\"141122\",\"141221\",\"112214\",\"112412\",\"122114\",\n \"122411\",\"142112\",\"142211\",\"241211\",\"221114\",\"413111\",\"241112\",\"134111\",\n \"111242\",\"121142\",\"121241\",\"114212\",\"124112\",\"124211\",\"411212\",\"421112\",\n \"421211\",\"212141\",\"214121\",\"412121\",\"111143\",\"111341\",\"131141\",\"114113\",\n \"114311\",\"411113\",\"411311\",\"113141\",\"114131\",\"311141\",\"411131\",\"211412\",\n \"211214\",\"211232\",\"2331112\"\n ];\n var syms=[], chk=0, pos=1, i;\n var allDigits = /^\\d+$/.test(text);\n if(allDigits \u0026\u0026 text.length%2===0){\n syms.push(105); chk=105;\n for(i=0;i\u003ctext.length;i+=2){\n var v=parseInt(text.substring(i,i+2));\n syms.push(v); chk+=pos*v; pos++;\n }\n } else if(allDigits \u0026\u0026 text.length%2===1){\n syms.push(104); chk=104;\n var v0=text.charCodeAt(0)-32;\n syms.push(v0); chk+=pos*v0; pos++;\n syms.push(99); chk+=pos*99; pos++;\n for(i=1;i\u003ctext.length;i+=2){\n var v2=parseInt(text.substring(i,i+2));\n syms.push(v2); chk+=pos*v2; pos++;\n }\n } else {\n syms.push(104); chk=104;\n for(i=0;i\u003ctext.length;i++){\n var v3=text.charCodeAt(i)-32;\n if(v3\u003c0||v3>95) v3=0;\n syms.push(v3); chk+=(i+1)*v3;\n }\n }\n syms.push(chk%103);\n syms.push(106);\n var x=10, rects='';\n for(var j=0;j\u003csyms.length;j++){\n var pat=T[syms[j]];\n for(var k=0;k\u003cpat.length;k++){\n var w=parseInt(pat[k]);\n if(k%2===0) rects+='\u003crect x=\"'+x+'\" y=\"0\" width=\"'+w+'\" height=\"'+barHeight+'\" fill=\"#000\"/>';\n x+=w;\n }\n }\n x+=10;\n // 112mm wide, matching build.py's module_width 0.33mm across ~332 modules.\n return '\u003csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 '+x+' '+barHeight+'\" '\n +'preserveAspectRatio=\"none\" shape-rendering=\"crispEdges\" '\n +'style=\"width:100%;height:'+barHeight+'px;display:block\">'+rects+'\u003c/svg>';\n}\n\n// =============================================================================\n// DATE HELPERS\n// =============================================================================\nfunction todayFin() {\n var d=new Date();\n return pad(d.getDate())+\".\"+pad(d.getMonth()+1)+\".\"+d.getFullYear();\n}\nfunction pad(n){ return String(n).padStart(2,\"0\"); }\nfunction toFin(s) {\n if (!s) return \"\";\n s = String(s).trim();\n if (/^\\d{1,2}\\.\\d{1,2}\\.\\d{4}$/.test(s)) {\n var a=s.split(\".\"); return pad(a[0])+\".\"+pad(a[1])+\".\"+a[2];\n }\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(s)) {\n var b=s.split(\"-\"); return b[2]+\".\"+b[1]+\".\"+b[0];\n }\n if (/^\\d{1,2}\\/\\d{1,2}\\/\\d{4}$/.test(s)) {\n var c=s.split(\"/\"); return pad(c[1])+\".\"+pad(c[0])+\".\"+c[2];\n }\n return s;\n}\nfunction addDaysFin(finDate, days) {\n var p=finDate.split(\".\");\n var d=new Date(parseInt(p[2]), parseInt(p[1])-1, parseInt(p[0]));\n d.setDate(d.getDate()+days);\n return pad(d.getDate())+\".\"+pad(d.getMonth()+1)+\".\"+d.getFullYear();\n}\n\n// =============================================================================\n// NUMBER FORMAT\n// =============================================================================\nfunction round2(x){ return Math.round((parseFloat(x)+Number.EPSILON)*100)/100; }\n\n// Form fields (always Finnish style — the form is the bookkeeper's, not the\n// client's).\nfunction finNum(n){ return (parseFloat(n)||0).toFixed(2).replace(\".\",\",\"); }\n\n// ★ The printed document. Finnish: space thousands, comma decimal.\n// English: comma thousands, dot decimal. On an invoice this is not cosmetic —\n// \"3,281.50\" read as Finnish is a different number.\nfunction money(v, lang, dp){\n if (dp == null) dp = 2;\n var n = parseFloat(v); if (!isFinite(n)) n = 0;\n var neg = n \u003c 0; if (neg) n = -n;\n var parts = n.toFixed(dp).split(\".\");\n // U+00A0 = non-breaking space, so a Finnish amount never wraps mid-number.\n var thou = (lang === \"en\") ? \",\" : \"\\u00A0\";\n var dec = (lang === \"en\") ? \".\" : \",\";\n parts[0] = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, thou);\n return (neg ? \"-\" : \"\") + (parts.length > 1 ? parts[0] + dec + parts[1] : parts[0]);\n}\n// VAT rates print without trailing zeros: 25,5 / 13,5 / 10 / 0\nfunction pct(v, lang){\n // two decimals, as on the samples Hassan approved (25,50 % / 13,50 % / 0,00 %)\n return money(v, lang, 2);\n}\n\n// =============================================================================\n// LINE ITEMS\n// =============================================================================\nvar lineItemCount=0;\nfunction vatToCode(vat){\n if(vat===25.5||vat===\"25.5\") return \"3000\";\n if(vat===13.5||vat===\"13.5\") return \"3010\";\n if(vat===10 ||vat===\"10\") return \"3020\";\n return \"3030\";\n}\nfunction addLineItem(desc,qty,unit,price,vat,code,type) {\n lineItemCount++;\n var id=\"li_\"+lineItemCount;\n var tbody=document.getElementById(\"lineItemsBody\");\n var tr=document.createElement(\"tr\");\n tr.id=id;\n tr.innerHTML=\n '\u003ctd>\u003cinput type=\"text\" id=\"'+id+'_code\" value=\"'+(code||\"\")+'\">\u003c/td>'+\n '\u003ctd>\u003cinput type=\"text\" id=\"'+id+'_desc\" onchange=\"recalcRow(\\''+id+'\\')\" value=\"'+(desc||\"\")+'\">\u003c/td>'+\n '\u003ctd>\u003cinput type=\"text\" id=\"'+id+'_type\" value=\"'+(type||\"\")+'\" placeholder=\"(none)\" title=\"Blank = this row prints on its own\">\u003c/td>'+\n '\u003ctd>\u003cinput type=\"number\" id=\"'+id+'_qty\" min=\"0\" step=\"any\" onkeyup=\"recalcRow(\\''+id+'\\')\" onchange=\"recalcRow(\\''+id+'\\')\" value=\"'+(qty||1)+'\" style=\"text-align:right\">\u003c/td>'+\n '\u003ctd>\u003cinput type=\"text\" id=\"'+id+'_unit\" value=\"'+(unit||\"kpl\")+'\" style=\"text-align:center\">\u003c/td>'+\n '\u003ctd>\u003cinput type=\"number\" id=\"'+id+'_price\" min=\"0\" step=\"0.01\" onkeyup=\"recalcRow(\\''+id+'\\')\" onchange=\"recalcRow(\\''+id+'\\')\" value=\"'+(price||\"\")+'\">\u003c/td>'+\n '\u003ctd>\u003cselect id=\"'+id+'_vat\" onchange=\"recalcRow(\\''+id+'\\')\">'+vatOptions(vat)+'\u003c/select>\u003c/td>'+\n '\u003ctd>\u003cinput type=\"text\" id=\"'+id+'_net\" readonly style=\"text-align:right;background:#f8f8f8;color:#555\">\u003c/td>'+\n '\u003ctd>\u003cinput type=\"text\" id=\"'+id+'_gross\" readonly style=\"text-align:right;background:#f8f8f8;color:#555\">\u003c/td>'+\n '\u003ctd>\u003cbutton class=\"btn-remove\" onclick=\"removeRow(\\''+id+'\\')\">✕\u003c/button>\u003c/td>';\n tbody.appendChild(tr);\n recalcRow(id);\n}\nfunction vatOptions(sel){\n return [[\"25.5\",\"25,5 %\"],[\"13.5\",\"13,5 %\"],[\"10\",\"10 %\"],[\"0\",\"0 %\"]].map(function(o){\n return '\u003coption value=\"'+o[0]+'\"'+((!sel\u0026\u0026o[0]===\"25.5\")||sel==o[0]?\" selected\":\"\")+'>'+o[1]+'\u003c/option>';\n }).join(\"\");\n}\nfunction removeRow(id){ var r=document.getElementById(id); if(r) r.parentNode.removeChild(r); }\nfunction recalcRow(id){\n var row=document.getElementById(id); if(!row) return;\n // ★ Read by ID, never by position. The pre-2 Sep version indexed\n // querySelectorAll(\"input,select\") as inp[2]/inp[4]/inp[5]; inserting any\n // new column shifted every index and silently zeroed every line total.\n var f=function(sfx){ return document.getElementById(id+sfx); };\n var qty=parseFloat(f(\"_qty\").value)||0;\n var price=parseFloat(f(\"_price\").value)||0;\n var vat=parseFloat(f(\"_vat\").value)||0;\n // ★ a line on an invoice is a CENT amount. Rounding here, once, is what makes\n // line totals, per-rate subtotals and the grand total reconcile exactly.\n var net=round2(qty*price);\n var gross=round2(net*(1+vat/100));\n document.getElementById(id+\"_net\").value=finNum(net);\n document.getElementById(id+\"_gross\").value=finNum(gross);\n var codeEl=document.getElementById(id+\"_code\");\n if(!codeEl.value) codeEl.value=vatToCode(vat);\n}\nfunction getLineItems(){\n var rows=document.querySelectorAll(\"#lineItemsBody tr\"); var items=[];\n rows.forEach(function(tr){\n var f=function(sfx){ return document.getElementById(tr.id+sfx); };\n var vat=parseFloat(f(\"_vat\").value)||0;\n var qty=parseFloat(f(\"_qty\").value)||0;\n var price=parseFloat(f(\"_price\").value)||0;\n var net=round2(qty*price);\n items.push({\n code:f(\"_code\").value||vatToCode(vat), desc:f(\"_desc\").value,\n // ★ Type drives the page-1 grouping. Blank means the row prints as\n // itself. The grouping key is (type, vat, unit) — never type alone,\n // because one printed line carries one VAT rate and one unit.\n type:(f(\"_type\").value||\"\").trim(),\n qty:qty, unit:f(\"_unit\").value, price:price, vat:vat,\n net:net, gross:round2(net*(1+vat/100))\n });\n });\n return items;\n}\n\n// =============================================================================\n// ★ GROUPING — [STATED Hassan, 1 Sep 2026]: \"a separate 'Type' field.\n// If it's blank: no grouping.\"\n// =============================================================================\n// The key is (Type, VAT rate, Unit) — NOT Type alone. A printed line carries one\n// \"VAT %\" cell and one total, so merging 25,5 % with 0 % would state a rate that\n// is false for part of the amount. Unit is in the key for the same reason:\n// 4 008 kpl + 95 km is not a quantity.\n//\n// The Type is never parsed out of the description. An earlier draft split\n// \"Route 2570318 (Newspaper)\" on the bracket, which worked on exactly one\n// client's naming habit and would have mis-grouped silently on any other.\nfunction groupLines(items){\n var out={}, order=[];\n items.forEach(function(it){\n var label = it.type || it.desc;\n var k = label + \"\\u0000\" + it.vat.toFixed(2) + \"\\u0000\" + it.unit;\n if(!out[k]){\n out[k] = {label:label, qty:0, net:0, vat:it.vat, unit:it.unit,\n price:null, mixedPrice:false, n:0};\n order.push(k);\n }\n var g = out[k];\n g.qty = round2(g.qty + it.qty);\n g.net = round2(g.net + it.net);\n g.n++;\n if (g.price === null) g.price = it.price;\n else if (Math.abs(g.price - it.price) > 1e-9) g.mixedPrice = true;\n });\n var lines = order.map(function(k){ return out[k]; });\n // * The last column on page 1 shows the line amount INCLUDING VAT - what a\n // customer reads \"Yhteensa\" to mean, and what page 2 already prints.\n // A group is keyed on (label, VAT rate, unit), so every line inside it\n // carries ONE rate and this gross is exact for the group.\n // The totals block still computes VAT per RATE and rounds it once\n // (calcTotals, AVL 209 e sec. note above) - so this column can sit a cent away\n // from \"Maksettava yhteensa\". [STATED Hassan 22 Sep 2026] that is accepted:\n // it is what most invoicing software does, and the totals block is the\n // authority. Do NOT \"fix\" it by rounding the totals per line.\n lines.forEach(function(g){ g.gross = round2(g.net * (1 + g.vat / 100)); });\n return { lines: lines, collapsed: lines.some(function(g){ return g.n > 1; }) };\n}\n\n// =============================================================================\n// TOTALS — VAT per rate, rounded once. See the AVL 209 e § note above.\n// =============================================================================\nfunction calcTotals(items){\n var per={}, order=[];\n items.forEach(function(it){\n var k = it.vat.toFixed(2);\n if(!per[k]){ per[k] = {rate:it.vat, net:0}; order.push(k); }\n per[k].net = round2(per[k].net + it.net);\n });\n order.sort(function(a,b){ return parseFloat(a) - parseFloat(b); });\n var bands=[], net=0, vat=0;\n order.forEach(function(k){\n var b = per[k];\n var v = round2(b.net * b.rate / 100); // ★ rounded ONCE, per rate\n bands.push({rate:b.rate, net:b.net, vat:v});\n net = round2(net + b.net);\n vat = round2(vat + v);\n });\n return {bands:bands, net:net, vat:vat, gross:round2(net + vat)};\n}\n\n// =============================================================================\n// SENDER FIELDS\n// =============================================================================\nfunction getSenderFields(){\n return {\n companyName:document.getElementById(\"sCompanyName\").value,\n ytunnus: document.getElementById(\"sYtunnus\").value,\n address: document.getElementById(\"sAddress\").value,\n postalCity: document.getElementById(\"sPostalCity\").value,\n domicile: document.getElementById(\"sDomicile\").value,\n iban: document.getElementById(\"sIban\").value,\n bic: document.getElementById(\"sBic\").value,\n bankName: document.getElementById(\"sBankName\").value,\n phone: document.getElementById(\"sPhone\").value,\n email: document.getElementById(\"sEmail\").value,\n web: document.getElementById(\"sWeb\").value,\n pdfLang: document.getElementById(\"pdfLang\").value\n };\n}\nfunction setSenderFields(c){\n if(!c) return;\n function set(id,v){ if(v) document.getElementById(id).value=v; }\n set(\"sCompanyName\",c.companyName); set(\"sYtunnus\",c.ytunnus);\n set(\"sAddress\",c.address); set(\"sPostalCity\",c.postalCity);\n set(\"sDomicile\",c.domicile); set(\"sIban\",c.iban);\n set(\"sBic\",c.bic); set(\"sBankName\",c.bankName);\n set(\"sPhone\",c.phone); set(\"sEmail\",c.email);\n set(\"sWeb\",c.web);\n if (c.pdfLang === \"en\" || c.pdfLang === \"fi\") {\n document.getElementById(\"pdfLang\").value = c.pdfLang;\n }\n}\n// ALV-numero = \"FI\" + Y-tunnus without the hyphen.\nfunction vatNumberFromYtunnus(y){\n var d = String(y||\"\").replace(/[^0-9]/g,\"\");\n return d.length === 8 ? \"FI\" + d : \"\";\n}\n\n// =============================================================================\n// FORM INIT\n// =============================================================================\nvar invoiceNum = 1;\n\nwindow.onload = function() {\n document.getElementById(\"invoiceDate\").value = todayFin();\n updateDueDate();\n addLineItem();\n\n google.script.run\n .withSuccessHandler(function(setup) {\n if (setup \u0026\u0026 setup.error) {\n console.error(\"getInvoiceConfig error:\", setup.error);\n return;\n }\n invoiceNum = setup.invoiceNumber || 1;\n document.getElementById(\"invoiceNumber\").value = invoiceNum;\n updateViitenumero();\n setSenderFields(setup.config);\n setSenderFields(setup.saved);\n recipientProfiles = Array.isArray(setup.recipients) ? setup.recipients : [];\n populateProfileDropdown();\n })\n .withFailureHandler(function(e) { console.error(\"getInvoiceConfig failure:\", e.message); })\n .getInvoiceConfig(PE_CLIENT_ID);\n};\n\n// s356 [STATED Hassan 22 Oct 2026: \"The rest (e.g. IBAN) should be saved for next time on any computer.\"]\n// The sender block is saved in Scribe Core (client_invoice_settings, per client) as soon as a field\n// is changed -- not only when an invoice is made -- so an IBAN typed once is there next time, on\n// any computer. Saves are coalesced: one save 0.7 s after the last change.\nvar _senderSaveT = 0;\nfunction _senderSaveSoon(){\n clearTimeout(_senderSaveT);\n var tag = document.getElementById(\"senderSaved\"); if (tag) { tag.style.color = \"#888\"; tag.textContent = \"saving…\"; }\n _senderSaveT = setTimeout(function(){\n google.script.run\n .withSuccessHandler(function(r){\n var t = document.getElementById(\"senderSaved\"); if (!t) return;\n if (r \u0026\u0026 r.error) { t.style.color = \"#c0392b\"; t.textContent = \"not saved: \" + r.error; return; }\n t.style.color = \"#2e7d32\"; t.textContent = \"saved ✓\";\n })\n .withFailureHandler(function(e){\n var t = document.getElementById(\"senderSaved\"); if (t) { t.style.color = \"#c0392b\"; t.textContent = \"not saved: \" + (e \u0026\u0026 e.message ? e.message : e); }\n })\n .saveInvoiceSenderSettings(PE_CLIENT_ID, JSON.stringify(getSenderFields()));\n }, 700);\n}\n[\"sCompanyName\",\"sYtunnus\",\"sAddress\",\"sPostalCity\",\"sDomicile\",\"sIban\",\"sBic\",\"sBankName\",\"sPhone\",\"sEmail\",\"sWeb\",\"pdfLang\"].forEach(function(id){\n var el = document.getElementById(id); if (el) el.addEventListener(\"change\", _senderSaveSoon);\n});\n\ndocument.getElementById(\"invoiceDate\").addEventListener(\"input\", updateDueDate);\ndocument.getElementById(\"paymentTerms\").addEventListener(\"change\", updateDueDate);\ndocument.getElementById(\"invoiceNumber\").addEventListener(\"input\", updateViitenumero);\n\nfunction updateDueDate(){\n var d=document.getElementById(\"invoiceDate\").value;\n var days=parseInt(document.getElementById(\"paymentTerms\").value)||14;\n var fin=toFin(d);\n if(fin\u0026\u0026fin.indexOf(\".\")>0) document.getElementById(\"dueDate\").value=addDaysFin(fin,days);\n}\nfunction updateViitenumero(){\n var n=parseInt(document.getElementById(\"invoiceNumber\").value)||1;\n document.getElementById(\"viitenumero\").value=getViitenumero(n);\n}\nfunction backToForm(){\n document.getElementById(\"previewPanel\").style.display=\"none\";\n document.getElementById(\"formPanel\").style.display=\"block\";\n}\n\n// =============================================================================\n// HTML ESCAPE\n// =============================================================================\nfunction h(s){ return String(s==null?\"\":s).replace(/\u0026/g,\"\u0026amp;\").replace(/\u003c/g,\"\u0026lt;\").replace(/>/g,\"\u0026gt;\").replace(/\"/g,\"\u0026quot;\"); }\n\n// =============================================================================\n// ERRORS AND NOTES\n// =============================================================================\nfunction showFormError(msg){\n var el=document.getElementById(\"formError\");\n if(!el){ el=document.createElement(\"div\"); el.id=\"formError\";\n el.style.cssText=\"background:#ffd7d7;color:#900;border:1px solid #f00;border-radius:4px;padding:8px 12px;margin-bottom:10px;font-weight:bold\";\n document.getElementById(\"formPanel\").insertBefore(el,document.getElementById(\"formPanel\").firstChild);\n }\n el.textContent=msg; el.scrollIntoView({behavior:\"smooth\"});\n}\nfunction clearFormError(){ var el=document.getElementById(\"formError\"); if(el) el.textContent=\"\"; }\n\n// ★ Design for the failure being VISIBLE. Anything the renderer had to do\n// differently — no barcode, a demoted layout — is stated on screen, above the\n// preview, in a block that does not print.\nvar previewNotes = [];\nfunction note(msg){ previewNotes.push(msg); }\nfunction flushNotes(){\n var box = document.getElementById(\"previewNotes\");\n if (!previewNotes.length) { box.innerHTML = \"\"; return; }\n box.innerHTML = '\u003cdiv class=\"preview-note no-print\">⚠ ' +\n previewNotes.map(h).join('\u003cbr>⚠ ') + '\u003c/div>';\n}\n\n// =============================================================================\n// GENERATE INVOICE — side effects once, then render\n// =============================================================================\nvar LAST = null; // the payload of the last render, so the language can be\n // switched without re-saving or re-incrementing anything.\n\nfunction generateInvoice(){\n try {\n clearFormError();\n var lang = document.getElementById(\"pdfLang\").value === \"en\" ? \"en\" : \"fi\";\n var T = dict(lang);\n\n var allItems = getLineItems();\n var items = allItems.filter(function(it){ return it.desc || it.price > 0; });\n if(!items.length){ showFormError(T.NOITEMS); return; }\n\n var c = getSenderFields();\n var invNum = parseInt(document.getElementById(\"invoiceNumber\").value)||invoiceNum;\n var invDate = toFin(document.getElementById(\"invoiceDate\").value);\n var dueDate = toFin(document.getElementById(\"dueDate\").value);\n var terms = document.getElementById(\"paymentTerms\").value;\n var viitenro= document.getElementById(\"viitenumero\").value||getViitenumero(invNum);\n var lateInt = document.getElementById(\"lateInterest\").value||\"8\";\n var rec = getRecipientFields();\n\n // ── Save sender settings (fire-and-forget) ───────────────────────────────\n google.script.run\n .withSuccessHandler(function(){})\n .withFailureHandler(function(e){ console.warn(\"saveInvoiceSenderSettings:\", e.message); })\n .saveInvoiceSenderSettings(PE_CLIENT_ID, JSON.stringify(c));\n\n // ── Save recipient profile ───────────────────────────────────────────────\n if (document.getElementById(\"saveRecipient\").checked \u0026\u0026 rec.name) {\n var existingIdx = recipientProfiles.findIndex(function(p){\n return p.name \u0026\u0026 p.name.toLowerCase() === rec.name.toLowerCase();\n });\n var newProfile = { name:rec.name, ytunnus:rec.ytunnus, address:rec.address,\n postal:rec.postal, custNum:rec.custNum };\n if (existingIdx >= 0) recipientProfiles[existingIdx] = newProfile;\n else recipientProfiles.push(newProfile);\n populateProfileDropdown();\n saveRecipientProfiles();\n }\n\n // ── Issue the invoice in Core: Core gives the number and does the VAT ────\n var iso = function(f){ var a=String(f||\"\").split(\".\"); return a.length===3 ? a[2]+\"-\"+pad(a[1])+\"-\"+pad(a[0]) : \"\"; };\n var header = { invoice_date: iso(invDate), due_date: iso(dueDate), customer_name: rec.name,\n customer_y_tunnus: rec.ytunnus, customer_ref: rec.custNum };\n var lines = items.map(function(it){\n return { account_code: it.code, description: it.desc, quantity: it.qty,\n unit_price: it.price, net_amount: it.net };\n });\n var btn = document.querySelector(\"button[onclick^='generateInvoice']\");\n if (btn) btn.disabled = true;\n google.script.run\n .withSuccessHandler(function(res){\n if (btn) btn.disabled = false;\n if (!res || res.error || !res.invoice) {\n showFormError((res \u0026\u0026 res.error) || \"Scribe 2.0 did not issue the invoice, so nothing was saved.\");\n return;\n }\n try {\n invNum = parseInt(res.invoice.invoice_no) || invNum;\n viitenro = getViitenumero(invNum);\n LAST = {\n items:items, c:c, invNum:invNum, invDate:invDate, dueDate:dueDate,\n terms:terms, viitenro:viitenro, lateInt:lateInt, rec:rec,\n delDate: document.getElementById(\"deliveryDate\").value,\n remDays: document.getElementById(\"reminderDays\").value,\n period: document.getElementById(\"servicePeriod\").value,\n delTerms:document.getElementById(\"deliveryTerms\").value,\n delMeth: document.getElementById(\"deliveryMethod\").value\n };\n renderInvoice(lang);\n // the next invoice number is Core's; show it after this one is out\n invoiceNum = invNum + 1;\n document.getElementById(\"invoiceNumber\").value = invoiceNum;\n updateViitenumero();\n } catch(err2) { showFormError(dict(lang).ERR + err2.message); console.error(err2); }\n })\n .withFailureHandler(function(e){\n if (btn) btn.disabled = false;\n showFormError(\"Scribe 2.0 did not issue the invoice, so nothing was saved: \" + e.message);\n })\n .issueInvoice(PE_CLIENT_ID, JSON.stringify(header), JSON.stringify(lines));\n\n } catch(err) {\n showFormError(dict(document.getElementById(\"pdfLang\").value).ERR + err.message);\n document.getElementById(\"formPanel\").style.display=\"block\";\n document.getElementById(\"previewPanel\").style.display=\"none\";\n console.error(err);\n }\n}\n\n// ★ Re-render in the other language. NO side effects — the counter is not\n// incremented again and nothing is re-saved.\nfunction switchLang(lang){\n if (!LAST) return;\n document.getElementById(\"pdfLang\").value = lang;\n renderInvoice(lang);\n}\n\n// =============================================================================\n// RENDER\n// =============================================================================\nfunction renderInvoice(lang){\n var T = dict(lang);\n var d = LAST;\n var items = d.items;\n var c = d.c, rec = d.rec;\n\n // ★ The panel must be VISIBLE before anything is measured: a display:none\n // element reports every rectangle as zero, which reads as \"no clear air\"\n // and demotes a layout that would have fitted perfectly. Caught by the\n // headless render test, not by reading the code.\n document.getElementById(\"formPanel\").style.display = \"none\";\n document.getElementById(\"previewPanel\").style.display = \"block\";\n\n var tot = calcTotals(items);\n var gr = groupLines(items);\n var lines = gr.lines;\n var collapsed = gr.collapsed;\n\n // ── ★ THE PAGE-2 RULE — two INDEPENDENT triggers ─────────────────────────\n // (a) grouping collapsed rows -> page 2 carries the breakdown\n // (b) page-1 lines exceed capacity -> they cannot be shown on page 1\n //\n // (b) is the case that caused the original bug and that Hassan's first\n // draft rule missed: Type blank AND 40 rows. Nothing collapses, so no\n // breakdown is owed — but 40 rows do not fit and must go somewhere.\n //\n // On (b) page 1 falls back to ONE LUMP LINE PER VAT RATE — [STATED Hassan,\n // 2 Sep 2026]: \"page 1 should have at least something in my opinion, it\n // looks very odd as per the Finnish standards.\"\n // ★ That fallback is NOT self-evidently safe, contrary to build.py: with\n // four VAT rates it is four lump lines AND four totals rows, and it\n // misses by 17mm. What closes the rule is the measurement below, not a\n // count of lines.\n var lumped = lines.length > MAX_P1_LINES;\n if (lumped) lines = lumpByVat(tot, T);\n\n // First render.\n paint(lang, lines, collapsed || lumped, lumped);\n\n // ★ Then MEASURE, and demote if the header turned out taller than the\n // calibration assumed. build.py could not do this; a browser can.\n var clear = measureClear();\n if (clear !== null \u0026\u0026 clear \u003c MIN_CLEAR_PX \u0026\u0026 !lumped) {\n lumped = true;\n lines = lumpByVat(tot, T);\n paint(lang, lines, true, true);\n clear = measureClear();\n note(lang === \"en\"\n ? \"Page 1 was summarised by VAT rate because the detail lines do not fit above the giro form. The full line-by-line breakdown is on page 2.\"\n : \"Sivu 1 on koottu verokannoittain, koska rivit eivät mahdu tilisiirron yläpuolelle. Täydellinen erittely on sivulla 2.\");\n }\n // Last resort: even the lump layout collides. Scale the flowing part of\n // page 1 only — the footer stays pinned and full size.\n if (clear !== null \u0026\u0026 clear \u003c MIN_CLEAR_PX) {\n var flow = document.querySelector(\"#invoiceOutput .iv-p1 .iv-flow\");\n if (flow \u0026\u0026 flow.offsetHeight > 0) {\n var have = flow.offsetHeight;\n var want = have - (MIN_CLEAR_PX - clear);\n var s = Math.max(0.6, want / have);\n flow.style.transform = \"scale(\" + s + \")\";\n flow.style.height = Math.ceil(have * s) + \"px\";\n flow.setAttribute(\"data-fit\", s.toFixed(3));\n note(lang === \"en\"\n ? \"Page 1 was reduced to \" + Math.round(s*100) + \"% to keep the giro form on the page. Check it before sending.\"\n : \"Sivun 1 yläosaa pienennettiin \" + Math.round(s*100) + \" %:iin, jotta tilisiirto mahtuu sivulle. Tarkista lasku ennen lähetystä.\");\n }\n }\n\n numberPages(lang);\n flushNotes();\n\n document.getElementById(\"langFi\").style.fontWeight = (lang === \"fi\") ? \"800\" : \"600\";\n document.getElementById(\"langEn\").style.fontWeight = (lang === \"en\") ? \"800\" : \"600\";\n window.scrollTo(0, 0);\n}\n\nfunction lumpByVat(tot, T){\n // Qty, Unit and Unit price are left EMPTY on a lump line: one rate can span\n // kpl and km, and 4 008 kpl + 95 km is not a quantity. Stating a number there\n // would be worse than stating nothing.\n return tot.bands.map(function(b){\n return {label:T.LUMP, qty:null, net:b.net, vat:b.rate, unit:\"\",\n price:null, mixedPrice:false, n:0};\n });\n}\n\n// Clear air in px between the bottom of the totals block and the top of the\n// pinned footer, on page 1. Null when the elements are not present.\nfunction measureClear(){\n var p1 = document.querySelector(\"#invoiceOutput .iv-p1\");\n if (!p1) return null;\n var tw = p1.querySelector(\".totwrap\");\n var ft = p1.querySelector(\".foot\");\n if (!tw || !ft) return null;\n var pTop = p1.getBoundingClientRect().top;\n var twB = tw.getBoundingClientRect().bottom - pTop;\n var ftT = ft.getBoundingClientRect().top - pTop;\n return ftT - twB;\n}\n\nfunction numberPages(lang){\n var T = dict(lang);\n var pages = document.querySelectorAll(\"#invoiceOutput .iv-page\");\n // Page 2 flows, so it can occupy more than one printed sheet. Count them.\n var sheets = 0;\n Array.prototype.forEach.call(pages, function(p){\n sheets += Math.max(1, Math.ceil((p.offsetHeight - 2) / (297 * PX_PER_MM)));\n });\n var n = 0;\n Array.prototype.forEach.call(pages, function(p){\n var m = p.querySelector(\".pg\");\n n += 1;\n if (m) m.textContent = (sheets > 1) ? fill(T.PAGE, {a:n, b:sheets}) : \"\";\n });\n}\n\n// =============================================================================\n// PAINT — build the document HTML\n// =============================================================================\nfunction paint(lang, lines, needsP2, lumped){\n var T = dict(lang);\n previewNotes = []; // a re-paint replaces the notes, never appends\n var d = LAST, c = d.c, rec = d.rec, cur = \"€\";\n var items = d.items;\n var tot = calcTotals(items);\n\n // ── key/value table (right of the header) — optional rows only when filled ─\n var kv = [];\n function kvRow(label, value){\n if (value === \"\" || value == null) return;\n kv.push(\"\u003ctr>\u003cth>\" + h(label) + \"\u003c/th>\u003ctd>\" + h(value) + \"\u003c/td>\u003c/tr>\");\n }\n kvRow(T.KV_NO, d.invNum);\n kvRow(T.KV_DATE, d.invDate);\n kvRow(T.KV_TERMS, fill(T.DAYS, {n: d.terms}));\n kvRow(T.KV_DUE, d.dueDate);\n kvRow(T.KV_INT, (parseFloat(d.lateInt)||0).toFixed(2).replace(\".\", lang===\"en\"?\".\":\",\") + \" %\");\n kvRow(T.KV_REF, d.viitenro);\n kvRow(T.KV_CUST, rec.custNum);\n kvRow(T.KV_CUSTYT, rec.ytunnus);\n kvRow(T.KV_DELDATE, d.delDate);\n kvRow(T.KV_REMIND, d.remDays);\n kvRow(T.KV_DELTERMS, d.delTerms);\n kvRow(T.KV_DELMETH, d.delMeth);\n\n // ── page-1 line rows ──────────────────────────────────────────────────────\n var rows1 = lines.map(function(g){\n if (g.qty === null) { // lump line\n return \"\u003ctr>\u003ctd>\" + h(g.label) + \"\u003c/td>\" +\n '\u003ctd class=\"r\">' + h(d.invDate) + \"\u003c/td>\" +\n '\u003ctd class=\"r\">\u003c/td>\u003ctd class=\"c\">\u003c/td>\u003ctd class=\"r\">\u003c/td>' +\n '\u003ctd class=\"r\">' + pct(g.vat, lang) + \" %\u003c/td>\" +\n '\u003ctd class=\"r\">' + cur + money(g.gross, lang) + \"\u003c/td>\u003c/tr>\";\n }\n // ★ the unit price prints ONLY when the whole group shares one — otherwise\n // blank. A blended average would fail the first qty x price check anybody\n // does with a calculator.\n var up = g.mixedPrice ? \"\" : (cur + money(g.price, lang));\n return \"\u003ctr>\u003ctd>\" + h(g.label) + \"\u003c/td>\" +\n '\u003ctd class=\"r\">' + h(d.invDate) + \"\u003c/td>\" +\n '\u003ctd class=\"r\">' + money(g.qty, lang) + \"\u003c/td>\" +\n '\u003ctd class=\"c\">' + h(g.unit) + \"\u003c/td>\" +\n '\u003ctd class=\"r\">' + up + \"\u003c/td>\" +\n '\u003ctd class=\"r\">' + pct(g.vat, lang) + \" %\u003c/td>\" +\n '\u003ctd class=\"r\">' + cur + money(g.gross, lang) + \"\u003c/td>\u003c/tr>\";\n }).join(\"\");\n\n // ── totals block ──────────────────────────────────────────────────────────\n var totRows = ['\u003ctr>\u003ctd>' + h(T.EXVAT) + '\u003c/td>\u003ctd class=\"v\">' + cur + money(tot.net, lang) + \"\u003c/td>\u003c/tr>\"];\n tot.bands.forEach(function(b){\n totRows.push('\u003ctr>\u003ctd>' + h(T.VATPC.replace(\" %\",\"\")) + \" \" + pct(b.rate, lang) +\n ' %\u003c/td>\u003ctd class=\"v\">' + cur + money(b.vat, lang) + \"\u003c/td>\u003c/tr>\");\n });\n totRows.push('\u003ctr>\u003ctd>' + h(T.VATTOT) + '\u003c/td>\u003ctd class=\"v\">' + cur + money(tot.vat, lang) + \"\u003c/td>\u003c/tr>\");\n totRows.push('\u003ctr class=\"due\">\u003ctd>' + h(T.DUEAMT) + '\u003c/td>\u003ctd class=\"v\">' + cur + money(tot.gross, lang) + \"\u003c/td>\u003c/tr>\");\n var totalsHtml = totRows.join(\"\");\n\n // ── barcode ───────────────────────────────────────────────────────────────\n var bc = buildBarcodeString(c.iban, tot.gross, d.viitenro, d.dueDate);\n var bcSvg = bc.str ? generateCode128SVG(bc.str, 40) : \"\";\n if (!bc.str) {\n note(lang === \"en\"\n ? \"No payment barcode was printed because \" + bc.reasons.join(\"; \") +\n \". The giro form is still valid without it.\"\n : \"Viivakoodia ei tulostettu, koska \" + bc.reasons.join(\"; \") +\n \". Tilisiirtolomake on silti pätevä ilman sitä.\");\n }\n\n var vatNo = vatNumberFromYtunnus(c.ytunnus);\n var conum = h(c.ytunnus) + (vatNo ? \" / \" + vatNo : \"\");\n var seeP2 = needsP2 ? fill(T.SEEP2, {n: items.length}) : \"\";\n\n // ── PAGE 1 ────────────────────────────────────────────────────────────────\n var p1 =\n '\u003cdiv class=\"iv-page iv-p1\">' +\n '\u003cdiv class=\"iv-flow\">' +\n '\u003cdiv class=\"head\">' +\n '\u003cdiv class=\"from\">\u003cspan class=\"nm\">' + h(c.companyName) + \"\u003c/span>\u003cbr>\" +\n h(c.address) + \"\u003cbr>\" + h(c.postalCity) + \"\u003c/div>\" +\n '\u003cdiv class=\"to\">' + h(rec.name) + \"\u003cbr>\" + h(rec.address) + \"\u003cbr>\" + h(rec.postal) + \"\u003c/div>\" +\n '\u003ch1 class=\"inv-title\">' + h(T.INVOICE) + \"\u003c/h1>\" +\n '\u003cdiv class=\"pg\">\u003c/div>' +\n '\u003cdiv class=\"kvwrap\">\u003ctable class=\"kv\">' + kv.join(\"\") + \"\u003c/table>\u003c/div>\" +\n \"\u003c/div>\" +\n (d.period ? '\u003cdiv class=\"period\">' + h(d.period) + \"\u003c/div>\" : '\u003cdiv class=\"period\">\u0026nbsp;\u003c/div>') +\n '\u003ctable class=\"lines\">\u003cthead>\u003ctr>' +\n \"\u003cth>\" + h(T.DESC) + '\u003c/th>\u003cth class=\"r\">' + h(T.DATE) + '\u003c/th>\u003cth class=\"r\">' + h(T.QTY) + \"\u003c/th>\" +\n '\u003cth class=\"c\">' + h(T.UNIT) + '\u003c/th>\u003cth class=\"r\">' + h(T.PRICE) + \"\u003c/th>\" +\n '\u003cth class=\"r\">' + h(T.VATPC) + '\u003c/th>\u003cth class=\"r\">' + h(T.TOTAL) + \"\u003c/th>\" +\n \"\u003c/tr>\u003c/thead>\u003ctbody>\" + rows1 + \"\u003c/tbody>\u003c/table>\" +\n '\u003cdiv class=\"totwrap\">' +\n '\u003cdiv class=\"totleft\">' + h(d.period) +\n '\u003cdiv style=\"font-style:normal;font-size:8pt;color:#333;margin-top:4mm\">' + h(seeP2) + \"\u003c/div>\" +\n \"\u003c/div>\" +\n '\u003ctable class=\"tot\">' + totalsHtml + \"\u003c/table>\" +\n \"\u003c/div>\" +\n \"\u003c/div>\" +\n // ── the pinned footer. Its position does not depend on row count. ───────\n '\u003cdiv class=\"foot\">' +\n '\u003cdiv class=\"rule\">\u003c/div>' +\n '\u003cdiv class=\"info\">' +\n '\u003cdiv class=\"col\">' +\n '\u003cdiv class=\"r\">\u003cdiv class=\"l\">' + h(T.COMPANY) + '\u003c/div>\u003cdiv class=\"v\">' + h(c.companyName) + \"\u003c/div>\u003c/div>\" +\n '\u003cdiv class=\"r\">\u003cdiv class=\"l\">' + h(T.ADDRESS) + '\u003c/div>\u003cdiv class=\"v\">' + h(c.address) + (c.postalCity ? \", \" + h(c.postalCity) : \"\") + \"\u003c/div>\u003c/div>\" +\n '\u003cdiv class=\"r\">\u003cdiv class=\"l\">' + h(T.CONUM) + '\u003c/div>\u003cdiv class=\"v\">' + conum + \"\u003c/div>\u003c/div>\" +\n \"\u003c/div>\" +\n '\u003cdiv class=\"col b\">' +\n '\u003cdiv class=\"r\">\u003cdiv class=\"l\">' + h(T.TEL) + '\u003c/div>\u003cdiv class=\"v\">' + h(c.phone) + \"\u003c/div>\u003c/div>\" +\n '\u003cdiv class=\"r\">\u003cdiv class=\"l\">' + h(T.EMAIL) + '\u003c/div>\u003cdiv class=\"v\">' + h(c.email) + \"\u003c/div>\u003c/div>\" +\n '\u003cdiv class=\"r\">\u003cdiv class=\"l\">' + h(T.WWW) + '\u003c/div>\u003cdiv class=\"v\">' + h(c.web) + \"\u003c/div>\u003c/div>\" +\n \"\u003c/div>\" +\n \"\u003c/div>\" +\n '\u003cdiv class=\"cut\">\u003c/div>' +\n '\u003ctable class=\"giro\">' +\n '\u003ccolgroup>\u003ccol style=\"width:24mm\">\u003ccol style=\"width:82mm\">\u003ccol>\u003c/colgroup>' +\n \"\u003ctr>\" +\n '\u003ctd class=\"lab\">' + GIRO.ACCT + \"\u003c/td>\" +\n '\u003ctd>\u003cspan class=\"tag\">IBAN\u003c/span>\u003cspan class=\"big\">' +\n (c.bankName ? h(c.bankName) + \" \u0026nbsp; \" : \"\") + h(c.iban) + \"\u003c/span>\u003c/td>\" +\n '\u003ctd>\u003cspan class=\"tag\">' + GIRO.BIC + '\u003c/span>\u003cspan class=\"big\">' + h(c.bic) + \"\u003c/span>\u003c/td>\" +\n \"\u003c/tr>\" +\n \"\u003ctr>\" +\n '\u003ctd class=\"lab\">' + GIRO.RECIP + \"\u003c/td>\" +\n '\u003ctd class=\"cellh\">' + h(c.companyName) + \"\u003cbr>\" + h(c.address) + \"\u003cbr>\" + h(c.postalCity) + \"\u003c/td>\" +\n '\u003ctd rowspan=\"2\">\u003c/td>' +\n \"\u003c/tr>\" +\n \"\u003ctr>\" +\n '\u003ctd class=\"lab\">' + GIRO.PAYER + \"\u003c/td>\" +\n '\u003ctd class=\"cellh\" style=\"border-bottom:none\">' + h(rec.name) + \"\u003cbr>\" + h(rec.address) + \"\u003cbr>\" + h(rec.postal) + \"\u003c/td>\" +\n \"\u003c/tr>\" +\n \"\u003ctr>\" +\n '\u003ctd class=\"lab\">' + GIRO.SIG + \"\u003c/td>\" +\n '\u003ctd style=\"border-top:none\">\u003cdiv class=\"sig\">\u003c/div>\u003c/td>' +\n '\u003ctd>\u003cspan class=\"tag\">' + GIRO.REF + '\u003c/span>\u003cspan class=\"big\">' + h(d.viitenro) + \"\u003c/span>\u003c/td>\" +\n \"\u003c/tr>\" +\n \"\u003ctr>\" +\n '\u003ctd class=\"lab\">' + GIRO.FROM + \"\u003c/td>\" +\n '\u003ctd style=\"height:11mm\">\u003c/td>' +\n '\u003ctd style=\"padding:0\">' +\n '\u003ctable style=\"width:100%;border-collapse:collapse\">\u003ctr>' +\n '\u003ctd style=\"border:none;border-right:1px solid #000;padding:1.5mm 2mm;width:55%\">' +\n '\u003cspan class=\"tag\">' + GIRO.DUE + '\u003c/span>\u003cspan class=\"big\">' + h(d.dueDate) + \"\u003c/span>\u003c/td>\" +\n '\u003ctd style=\"border:none;padding:1.5mm 2mm\">' +\n '\u003cspan class=\"tag\">' + GIRO.TOT + '\u003c/span>\u003cspan class=\"big\">' + money(tot.gross, lang) + \"\u003c/span>\u003c/td>\" +\n \"\u003c/tr>\u003c/table>\" +\n \"\u003c/td>\" +\n \"\u003c/tr>\" +\n \"\u003c/table>\" +\n '\u003cdiv class=\"bcwrap\">' +\n '\u003cdiv class=\"bc\">' +\n (bc.str ? '\u003cdiv class=\"bcdigits\">' + bc.str + \"\u003c/div>\" + bcSvg : \"\") +\n \"\u003c/div>\" +\n '\u003cdiv class=\"terms\">' + GIRO.TERMS + \"\u003c/div>\" +\n \"\u003c/div>\" +\n \"\u003c/div>\" +\n \"\u003c/div>\";\n\n // ── PAGE 2 — the full line-by-line breakdown. Flows; never clipped. ───────\n var p2 = \"\";\n if (needsP2) {\n var rows2 = items.map(function(it){\n return \"\u003ctr>\u003ctd>\" + h(it.desc) + \"\u003c/td>\u003ctd>\" + h(it.type) + \"\u003c/td>\" +\n '\u003ctd class=\"r\">' + money(it.qty, lang) + \"\u003c/td>\" +\n '\u003ctd class=\"c\">' + h(it.unit) + \"\u003c/td>\" +\n '\u003ctd class=\"r\">' + cur + money(it.price, lang) + \"\u003c/td>\" +\n '\u003ctd class=\"r\">' + cur + money(it.net, lang) + \"\u003c/td>\" +\n '\u003ctd class=\"r\">' + pct(it.vat, lang) + \" %\u003c/td>\" +\n '\u003ctd class=\"r\">' + cur + money(it.gross, lang) + \"\u003c/td>\u003c/tr>\";\n }).join(\"\");\n p2 =\n '\u003cdiv class=\"iv-page iv-p2\">' +\n '\u003cdiv class=\"pg\" style=\"right:10mm\">\u003c/div>' +\n '\u003ch2 class=\"bd\">' + h(T.BREAKDOWN) + \"\u003c/h2>\" +\n '\u003cp class=\"sub\">' + h(c.companyName) + \" \u0026nbsp;\u0026bull;\u0026nbsp; \" + h(T.INVOICE_N) + \" \" +\n h(d.invNum) + (d.period ? \" \u0026nbsp;\u0026bull;\u0026nbsp; \" + h(d.period) : \"\") + \"\u003c/p>\" +\n '\u003ctable class=\"lines\">\u003cthead>\u003ctr>' +\n \"\u003cth>\" + h(T.DESC) + \"\u003c/th>\u003cth>\" + h(T.TYPE) + '\u003c/th>\u003cth class=\"r\">' + h(T.QTY) + \"\u003c/th>\" +\n '\u003cth class=\"c\">' + h(T.UNIT) + '\u003c/th>\u003cth class=\"r\">' + h(T.PRICE) + \"\u003c/th>\" +\n '\u003cth class=\"r\">' + h(T.NET) + '\u003c/th>\u003cth class=\"r\">' + h(T.VATPC) + \"\u003c/th>\" +\n '\u003cth class=\"r\">' + h(T.TOTAL) + \"\u003c/th>\" +\n \"\u003c/tr>\u003c/thead>\u003ctbody>\" + rows2 + \"\u003c/tbody>\u003c/table>\" +\n '\u003ctable class=\"p2tot\">' + totalsHtml + \"\u003c/table>\" +\n \"\u003c/div>\";\n }\n\n document.getElementById(\"invoiceOutput\").innerHTML = p1 + p2;\n}\n\u003c/script>\n\u003c/body>\n\u003c/html>\n"
The rules every EcoBooks bookkeeper follows, in plain English, each with the law or official guidance it comes from, and a guide to every screen of Scribe 2.0 (sections 9 to 12). This is the Rules tab in Scribe 2.0. Scribe 2.0 handles toiminimi (sole trader) clients; Oy companies come in a later release.
We book from documents. Every entry comes from a document the client uploads: an invoice, a receipt, a statement from Wolt or Bolt, a loan agreement. We do not book from bank statements, and we do not track whether a sales invoice has been paid. A sale is booked when the invoice is issued, a cost when the receipt or invoice is dated.
Toiminimi clients: the counterpart is the private account 2365, not the bank 1910. Our toiminimi clients pay and get paid through their personal bank account, so the bank account is not in their books. The law says that money moving between the entrepreneur and the business is booked on the private account, which is part of the owner's equity.
A sale: Dr 2365 (gross) / Cr sales account (net) / Cr 2939 VAT.
A cost: Dr cost account (net) + Dr 1763 VAT / Cr 2365 (gross).
The profit is the same either way — 2365 and 1910 are both balance-sheet accounts, so this choice never changes a client's result.
Source: KILA statement 1717/2003, section 3.3: transactions between the entrepreneur and the business are booked on the business's yksityistili, which belongs to its own equity.
Oy clients have their own business bank account. Their bank is 1910, and the private account is never used for an Oy. When Smart Post books an Oy's sale, the debit goes to 1701 (sales receivables), not to the bank; a cost is credited to 1910.
How Smart Post decides. Smart Post follows the same rules as the old posting engine. Besides the counterpart above:
The folder decides the direction. A document in Expense Receipts is never booked as income, and one in Sales Invoices is never booked as a cost. If the reading disagrees with the folder, the entry goes to REVIEW. One exception: a document in Expense Receipts that reads like income but is billed to the client by name is booked as a cost on 4600 (Ulkopuoliset palvelut), with a note to set the right account.
The folder decides the month. A document is booked in the month of the folder it was uploaded to, whatever date it carries. A document with no date is booked in that month too, and is not flagged.
No invoice number? The file name becomes the entry's reference. This is not flagged either.
The bill's own VAT rate is kept. Smart Post never changes the rate a bill shows. Sales and purchases go to the account for that rate (for example 3010 for sales at 13.5 %). If our chart has no account for that rate, the amount waits on 1799 with a review flag. The rates are set by Arvonlisäverolaki 84 §, 85 § and 85 a §.
A client outside the VAT register on the bill's date (the date set with Change VAT period in Clientele counts): no VAT is booked. A cost is booked at its full amount with no VAT deducted, and is not flagged. A sale is booked at its full amount and flagged, because a seller outside the VAT register must not show VAT on an invoice and must send the customer a corrected one (Vero: VAT invoice requirements, section 6.1.10).
A vehicle bought outright (kauppakirja) is booked to 1160 Ajoneuvokalusto. Its VAT is deducted only when the law allows it (section 4); otherwise the VAT is added to the vehicle's cost and the entry goes to REVIEW.
The same receipt in two different files: both are booked with their real amounts and the second is flagged POSSIBLE DUPLICATE. If it really is a duplicate, delete one entry. The same file read a second time is never booked twice.
Only accounts in our chart. Smart Post books only to live accounts in the Chart of Accounts. If a reading ever names any other account, the amount waits on 1799 with a review flag, and we fix the cause.
A flag means a real risk. Smart Post flags an entry only when a person has to decide something. It never flags what it can settle safely by itself. A bill the reader was only fairly sure it read right is a real risk, so it is flagged.
Always REVIEW, never booked automatically: tax payments (Verohallinto), driving-school and qualification fees, inspection and taxi-licence fees, card-terminal slips, restaurant receipts, loan instalments, and payout reports that refer to another invoice.
A closed month is never touched. Smart Post does not start on a closed month: a message says the month is closed, and nothing is read or booked. Reopen the month first (section 9.3).
REVIEW entries and 1799 Selvittelytili. A REVIEW entry is posted with its amount on 1799 Selvittelytili (suspense — "waiting for the right account") and a short note in plain words saying what is wrong and what to do (section 9.3), shown on the entry in Posted entries. Move the amount to the right account with ✎ Fix. Scribe 2.0 keeps the entry flagged for review until nothing is left on 1799, so an amount on 1799 is never a finished booking. 1799 is a real account in the chart (current assets); it should be empty once a month is done.
Documents Smart Post could not book. Nothing is left out silently. A document that cannot be booked at all is listed under its Smart Post in the Posting queue, and shows in that month's Posted entries as a flagged 0,00 line with the file name, a link to the file and the reason. It is a list, not a booking: a 0,00 line never enters the books. Retry reads that one document again; once it is booked, it disappears from both places. If there is truly nothing to book, press Nothing to book: it leaves both places, and who pressed it and when are kept. Never delete the file from the folder to clear the list: it is part of the client's accounting records and must be kept (Kirjanpitolaki 2:10 §). A file that Google Drive never shows to Scribe 2.0 cannot be on the list, so check that every file is in the right month's folder.
Why it was not booked
What to do
Scribe 2.0 could not open the file from Google Drive.
Check the file is still in the month's folder, then press Retry.
Scribe 2.0 could not read the file (blurry, password-protected, or not a bill).
Open it. Ask the client for a clear copy and press Retry, or book it by hand with + New voucher.
The amounts come to zero or less (a credit note or a refund).
Open it. Book a credit note by hand with + New voucher. For a slip with nothing to book, press Nothing to book.
It is a finance or loan agreement.
Book the purchase with + New voucher → Loan, then add the loan in Loans (sections 10 and 11).
2. Opening balances and new clients
A new year always opens with last year's closing balance sheet. The opening balance of every account is the balance it had on 31 December. This is the law (Kirjanpitolaki 3:3 §, point 5).
How a toiminimi's equity is carried into the new year:
Last year's profit or loss (2371) goes to 2251 (previous years' profit).
Last year's money put in (2361) and drawings (2365) go to 2370 (owner's equity).
The year's own accounts 2361 and 2371 start again at zero, and so does 2365 — with one exception: any balance left on 1910 (bank) at the end of the old year moves into 2365, same amount, opposite sign.
Example: 1910 closes at +12 580,28, so the new year opens with 1910 at 0,00 and 2365 at −12 580,28. Scribe 2.0's Import opening balances screen does this for you — you do not post it by hand.
In Scribe 2.0, open Import opening balances for the new year and press Carry forward. It replaces the opening column; it does not add to it.
Opening VAT: type it exactly as last year's closing balance sheet shows it, and do not clear it by hand. No manual February entry to clear last year's VAT. Scribe 2.0's VAT method will clear it on its due date; a hand-made clearing entry on top would clear the same VAT twice. The new method is not switched on yet, so until you are told it is, opening VAT simply stays on the balance sheet. That is expected. If you already posted a clearing entry under the old advice, do not delete it: tell Hassan the client and the entry.
A client who joins mid-year (from another accountant):
The year still starts on 1 January. The opening balances are the previous accountant's closing balance sheet at 31 December — never the balances on the day they joined.
January up to the month they joined is entered from the previous accountant's journal: in full where possible, otherwise one summary entry per month per account.
Three checks must pass before we continue: our profit and loss for those months equals theirs, our balance sheet on the joining date equals theirs, and our VAT per month equals the VAT returns they filed.
Fixed assets and loans go on the Assets and Loans tabs with their 1 January values.
3. VAT basics
Who must register for VAT. A business is not VAT-liable if its turnover is at most 20 000 € in both the current and the previous calendar year, unless it has registered voluntarily (Arvonlisäverolaki 3 §).
If turnover goes over 20 000 € during the year, VAT is due from that moment: the sale that crosses the limit is already taxed in full. Do not wait for the next year (Vero: small-scale business and VAT).
Clients with NO VAT. Always check the client's VAT registration in YTJ ("Arvonlisäverovelvollisuus"). If the client is not in the VAT register: no VAT on sales, no VAT deducted on costs, no monthly VAT settlement, and the VAT period field is left empty.
at most 30 000 € (a farmer, other primary producer or visual artist with no other VAT-liable business always has a yearly period, OVML 11 §)
A change to a longer period starts on 1 January of the year after the application.
A change to a shorter period starts at the beginning of the next shorter period. The months of the old period before the change are reported and paid earlier than usual.
A period must stay the same for at least one year.
If turnover goes over the limit, the client must tell Vero straight away; Vero can also change the period itself.
Deadlines (OVML 17 §): the VAT return and payment are due on the 12th of the second month after the period (January's VAT by 12 March). Annual filers: by the end of February of the next year. If the 12th is not a banking day, the due date is the next banking day (OVML 2 §).
We book VAT by document date, never by payment date. Payment-basis VAT (Arvonlisäverolaki 137 §) is not used at EcoBooks.
Purchases from outside the EU (online shops, China, the UK and others). Finnish VAT is 25.5 % (or a reduced Finnish rate). Any other rate on an invoice is not Finnish VAT and is never deducted.
Temu, Shein, AliExpress and similar (a parcel of 150 € or less, VAT shown at checkout). This is IOSS VAT. It is never deducted, even for a VAT-registered client. Book the whole amount as the cost at 0 % VAT (Vero: distance sales of goods, section 5.2).
Foreign VAT on any invoice (for example 20 %, another country's rate or a seller's own "tax" line). Never deducted. It is part of the price: book the whole amount at 0 %.
Goods imported for business by a VAT-registered client (usually anything over 150 €). Customs does not charge the VAT. The client reports 25.5 % import VAT on the customs value in the VAT return, under "Imports of goods from outside the EU" and "Tax on imports of goods from outside the EU", and deducts the same amount in the same return. This needs the customs clearance decision in the client's business name (Vero: import VAT is filed and paid on your own initiative).
VAT paid to Customs at the border (the client is not VAT-registered, or imported privately). Not deducted in the VAT return. It is part of the cost (Vero: VAT on imported goods).
Customs duty and anti-dumping duty (bicycles and e-bikes from China carry high anti-dumping duties) are part of the cost, never VAT.
An invoice re-issued after the purchase to add VAT that was not on the original receipt: book from the original receipt, never deduct the added VAT, and keep the re-issued invoice in the file with the note "Re-issued at the client's request after the purchase. Not used for booking or VAT."
Clients below the VAT threshold (not in the VAT register). They charge no VAT on sales and deduct no VAT on purchases: every VAT they pay is part of the cost (Vero: VAT-free small business). Two exceptions make them pay VAT anyway:
Services bought from a seller abroad (for example platform or app fees invoiced by a company in another country). The buyer owes 25.5 % Finnish VAT on them (reverse charge), the 20 000 € threshold does not apply, and the client must register for VAT with the role "ostoista tai omasta käytöstä". This VAT is never deducted, because the client's own sales are VAT-free (Vero: VAT guide, sections 2.4.1 and 2.4.5). Check every new client's foreign invoices for this.
Goods bought from other EU countries over 10 000 € (without VAT) in a calendar year. The client becomes liable for VAT on those purchases (same guide, section 2.4.2).
A client below the threshold may register voluntarily and then charges and deducts VAT like any registered business. A client who is not registered must never put VAT on an invoice.
4. Cars and other vehicles
VAT on a passenger car — the purchase and its running costs (fuel, repairs, tyres, washing, inspection, parking):
The VAT cannot be deducted, unless the car is used only for the business. Even a little private driving removes the whole deduction. The exception is a car bought for resale, for rental, for professional passenger transport (taxi) or for driving lessons: its VAT is deductible for the business driving, and only the private share is not (Arvonlisäverolaki 114 §, point 5 and 2nd paragraph).
We go by what the client tells us about business use. Ask the client to confirm it in writing (one line by email is enough) and keep that message with the client's documents.
For an ordinary passenger car with any declared private driving, book the whole price including VAT as the cost and deduct no VAT. For a taxi, driving-school, rental or resale car, deduct the VAT for the business share.
Vans, lorries, buses, mopeds, bicycles, e-bikes and e-scooters: VAT is deductible for the share used for the business (Arvonlisäverolaki 117 §).
A motorcycle follows the passenger-car rule above, not the business-share rule.
Income tax: actual costs or kilometre allowance.
If business driving is more than half of all driving in the year, the car belongs to the business: its actual costs are deducted (business share) and it goes on the Asset Register.
If private driving is 50 % or more, the car is private: the client may deduct business trips as a kilometre allowance instead (0,55 €/km in 2026), but not the car's actual costs.
Either way the client needs a mileage log (section 5).
Paid in full: Dr 1160 (price without deductible VAT) / Cr 2365. Add it to the Asset Register.
Check every vehicle purchase Smart Post books: it deducts any VAT printed on the document. For an ordinary passenger car whose VAT is not deductible, open the entry with ✎ Fix and put that VAT on 1160 instead of 1763.
Financed: use the + New voucher Loan voucher: the full price on 1160, the financed part on the loan account, the down payment as paid. Create the schedule in Loans and add the asset at its full price in Assets.
Traded in: the price of the new vehicle goes on 1160; the trade-in price of the old one goes in the Sold / traded in column of the Asset Register.
5. Mileage log (ajopäiväkirja)
Every client who drives for work keeps a mileage log. EcoBooks puts a ready-made log sheet in each client's shared Documents folder. For every business trip the log must show (Vero guidance 48192):
date, start time and end time;
where the trip started and ended, and the route when the start and end alone do not explain the distance;
the kilometres driven;
the purpose of the trip.
On the year tab the client also writes the odometer reading on 1 January and on 31 December, so the year's total and the business share can be worked out.
Regular trips that repeat in the same way may be written once with the number of times driven. Without any reliable record of business driving, Vero may refuse the kilometre allowance and the car costs altogether.
Taxi drivers also record, month by month, how many kilometres were professional taxi driving and how many were other driving — the log has a separate page for this.
6. Fixed assets, depreciation and small purchases
What goes on the Asset Register: machines, equipment and vehicles with a useful life of more than three years. The exception is an item costing at most 1 200 € that is expensed at once under the small-purchase allowance below; once that allowance (3 600 € a year) is used up, cheaper items go on the register too.
Depreciation is booked automatically by Smart Post. Never post depreciation by hand — it would be booked twice.
Rate: up to 25 % a year of the remaining value of all machines and equipment together (EVL 30 §).
For tax, an asset taken into use during the year counts in full for that year, whatever the month. Scribe 2.0 does this only with the annual cadence; with monthly it books 1/12 a month from the month taken into use, so a July purchase gets 6/12 in its first year (section 11).
If the remaining value of the whole group is 1 200 € or less, it may be written off at once.
A car used for professional passenger transport (taxi) may instead be depreciated on its own, outside the pool: at most 25 % in the year it is taken into use, at most 20 % in each of the next two years and at most 15 % after that. When it is sold, the sale price is income of that year (EVL 33 §, 3rd and 4th paragraph).
Tax deductions for depreciation can never be larger than what is booked in the accounts (EVL 54 §, 2nd paragraph) — another reason the register must be complete.
An item costing at most 1 200 € may be expensed at once — but no more than 3 600 € of such items in total per year.
Separately, an item with a useful life of at most 3 years may be expensed in full whatever it cost.
Example: a 900 € phone and a 700 € laptop can both be expensed (1 600 € so far). Once the year's small purchases reach 3 600 €, any further item over the limit goes on the Asset Register.
A meal or gift for the entrepreneur or their family is private, not representation.
8. Year end and record keeping
The financial statements (tilinpäätös) are ready within 4 months of the end of the financial year (Kirjanpitolaki 3:6 §), dated and signed by the entrepreneur, or for an Oy by the board and managing director (3:7 §).
An Oy files its financial statements with the Finnish Patent and Registration Office within 8 months of the end of the financial year, usually together with its tax return; filing late costs a fee. A toiminimi files only if, in both the closing year and the year before, it went over two of these three limits: turnover 15 000 000 €, balance sheet total 7 500 000 €, 50 employees on average. It then files within 6 months.
Keep the financial statements, the books and the list of accounts for 10 years from the end of the financial year, and receipts and other material for 6 years from the end of the calendar year in which the financial year ended (2:10 §).
Every figure must be traceable both ways: from the document to the entry to the report, and back (2:6 §). Never post without a document: every entry must be based on a dated and numbered document (2:5 §).
9. Scribe 2.0, screen by screen
Getting in. Open Scribe 2.0 and press Sign in with Google with your EcoBooks account. If it says your account is not active, ask an administrator. The sun and moon switch at the top right changes between light and dark mode; Scribe 2.0 remembers your choice. Pressing the Scribe 2.0 name at the top left closes the open client and takes you back to the client picker.
Opening a client. Type a client number, name or Business ID in Find a client, narrow the list with Bookkeeper or Status if you need to, and pick the client in Client — it opens at once. Then choose the Year and Month you want to work in; every screen follows that choice. Picking another client keeps you on the screen and month you are on. Refreshing the page in your browser opens the same client again, on the same screen. A client that is dormant or an ex-client shows a banner on every screen.
Years. The Year list runs from 2020 to this year — just pick the year you want (next year is added to the list later). If Scribe 2.0 does not hold that year for the client yet, it sets it up for you: when the year before is there, its closing balances become the new year's opening balances; when it is not (a new client's first year, or older books), the year starts empty and you enter its opening balances on the Opening balances screen. A year before 2020 is added with Add an earlier year… at the bottom of Opening balances. Opening balances are taken when the year is set up: if you post more to the year before afterwards, press Carry forward on Import opening balances for the new year. The Year and Month boxes only show on the screens that use them.
The menu on the left. The arrow at its top keeps the menu open or folds it away. A screen that is about one client asks you to pick a client first.
Menu item
What it is for
Section
Dashboard
Your workload at a glance
9.1
Posting queue
The progress of your Smart Posts
9.2
Posted entries
The month's entries: Smart Post, vouchers, checking
9.3
P&L, Balance Sheet, VAT Return, General Ledger
Reports
9.4
Mileage Log
Kilometre allowance from the client's Driver's Log
9.5
Generate report
Report PDFs for a period
9.4
Generate invoice
A sales invoice for the client
9.6
Import Journal
A Netvisor journal into Scribe 2.0
9.7
Opening balances, Import opening balances
The year's opening balances
9.8
Assets, Loans
The asset and loan registers
11
Prepaid tax
The client's prepaid income tax (ennakkovero)
12
Chart of Accounts
Which account for which cost or income
9.9
Clientele
The client's details
9.10
HR Engine
Clients and staff (owner and HR manager only)
9.11
9.1 Dashboard
The Dashboard opens first. Four tiles show your active clients, the clients whose last month is still to post, the VAT returns not yet filed and the entries waiting for review. Months posted shows how many clients each month is finished for, Due next lists the next VAT deadlines, and below it you see your latest Smart Posts. A yellow box at the top lists clients with money on the unknown account (????): those books are not finished, and their balance sheet cannot balance, until each line is moved to the right account with ✎ Fix. Owners and admins can pick one bookkeeper with the Bookkeeper filter.
9.2 Posting queue
Your latest Smart Posts, newest first, each with its state: Queued (waiting its turn), In Progress, Done, Partly Done (some documents could not be booked automatically), Failed (Scribe 2.0 could not finish it — the reason is shown on the line) or Stopped (someone cancelled it). A green dot means Scribe 2.0 is still working on something. ✕ stops a Smart Post that has not started yet.
If a Smart Post could not book some documents, they are listed right under it, each with its file, the reason in plain words and a Retry button (section 1, Documents Smart Post could not book). A Smart Post that booked everything shows no list.
9.3 Posted entries
The month's entries, with the month total and the VAT summary underneath. The total line must read balanced ✓.
Smart Post. Press Smart Post <month> after the client's documents for the month are in their folder. Scribe 2.0 collects the documents, prepares the entries and posts them; the message under the button tells you where it is, and you can keep working meanwhile. If the month already has entries, it first asks what to do:
Update — keeps your locked entries and manual vouchers, re-books the entries that came from documents, and adds any new documents uploaded since.
Start over — removes every entry in the month, locked ones too, and books the whole month again from its documents. It warns you first and names the locked entries.
Cancel — changes nothing.
Smart Post always says which it is doing — Posting October for the first time… or Reposting October… — and ends with October is posted ✓ or October is reposted ✓.
Months brought over from the old Scribe work exactly the same way. Their entries arrive locked, so Update keeps them and books only documents nobody had booked yet. Unlock an entry and Update books it again from its document; Start over books the whole month again from its documents.
If it says some documents could not be booked automatically, press Smart Post again later or book them with + New voucher.
REVIEW entries. An entry that needs a person carries a REVIEW badge and a ⚠ line that says, in plain words, what is wrong and what to do. An amount Smart Post was unsure of sits on 1799 Selvittelytili (section 1, How Smart Post decides). Show: Needs review only lists just those. Deal with each one before the month is done.
The ⚠ line says
What to do
Scribe 2.0 was not fully sure it read this bill right.
Open the bill and check the amounts and VAT. Correct anything wrong with ✎ Fix.
This was in Expense Receipts but reads like a sale.
Check the bill. If it is a cost, book it to the right cost account with ✎ Fix.
This was in Sales Invoices but reads like a cost.
Check the document. If it is a sale, book it to the right sales account with ✎ Fix.
This reads like a sale, but its lines look like a cost.
Check whether it really is a sale, and correct the accounts with ✎ Fix.
Booked as a cost on 4600 because the bill is in the client's name.
Move it to the right cost account with ✎ Fix.
Scribe 2.0 read the total but not the account or VAT rate.
Book it to the right account and VAT rate with ✎ Fix.
The VAT doesn't add up: two VAT rates look mixed on one line (for example a delivery at 25.5 % and a tip at 0 %).
Split it into one line per VAT rate with ✎ Fix.
The same bill seems to be booked already, from another file (named on the line).
If it is the same purchase, delete this entry. If they are two real purchases, keep both.
A card payment slip, not a receipt.
If the shop's receipt for the same purchase is in the folder, delete this entry. If not, ask the client for the receipt.
A payment to Vero (the tax office).
Scribe 2.0 already books VAT payments and prepaid tax by itself (Payment of VAT in this section, and section 12). If this was one of those, delete this entry. Otherwise book it with ✎ Fix.
A driving-school, qualification or licence fee.
Ask the client what it was for. A cost of the work goes on a cost account; anything personal goes on 2365. Book it with ✎ Fix.
A vehicle inspection or taxi-licence fee.
A yearly inspection (katsastus) of the work car is a car cost (section 4). For a taxi licence or qualification fee, ask the client, then book it with ✎ Fix.
A restaurant receipt.
Ask the client what the meal was. Entertaining a business guest is representation (section 7); a meal for the client is private, on 2365. Book it with ✎ Fix.
A loan payment.
If the loan is in Loans, Scribe 2.0 books its instalments by itself: delete this entry. If not, add the loan in Loans (section 11).
A payment report for an invoice that is booked on its own.
Delete this entry; the invoice is what gets booked.
The VAT on this vehicle was not deducted.
Check section 4. Only for a taxi, driving-school, rental or resale car, move the VAT to 1763 with ✎ Fix.
The amount paid is less than the vehicle's price; it may be bought on finance.
Delete this entry and book it with + New voucher → Loan, then add the loan and the asset (sections 10 and 11).
The client was outside the VAT register on this date, but the invoice shows VAT. It is booked without VAT.
Ask the client to send the customer a corrected invoice without VAT.
This bill's VAT rate has no account of its own in our chart.
Move the amount from 1799 to the right account with ✎ Fix.
Scribe 2.0 could not place this line on an account in our chart.
Move it from 1799 to the right account with ✎ Fix.
The buttons above the entries:
+ New voucher — book something by hand (section 10).
Close month / Reopen month — closing stops changes to a finished month; reopening needs a reason.
Show — all entries, or only those that need review.
🔒 Lock all / 🔓 Unlock all — lock or unlock every entry of the month. A locked entry keeps your changes when the month is updated.
🗑 Delete all — removes every entry of the month; you type the month name to confirm.
♻ Deleted entries — the entries deleted from this month. Restore puts one back exactly as it was.
On each entry:
✎ Fix — opens the entry in the voucher form to correct it; when you post, the fixed version replaces the old one and stays as you made it.
🔒 Lock / 🔓 Unlock — protects a checked entry from being re-booked.
💬 Ask — asks the built-in helper why the entry is flagged and how to book it. Enter sends, Shift+Enter starts a new line. ✎ Apply as Fix opens the suggested lines in the voucher form for you to check and post.
🗑 — deletes the entry (it goes to Deleted entries, from where it can be restored).
📎 file name — opens the document behind the entry in a window over the page; Close or Esc returns you to the entries. Open in a new tab is there if you want it beside the composer. If the window shows a Google notice about cookies instead of the document (some browsers, such as Zen, block Google inside another page), use Open in a new tab.
VAT summary. Under the entries you see the month's VAT: sales by rate, VAT on purchases, EU reverse charge and the amount to pay or reclaim. When the return for the whole VAT period has been filed in OmaVero, press ✓ Close VAT period to record it; every month of that period is marked filed.
Payment of the VAT. Scribe 2.0 counts each period's VAT as paid on its due date. Under the VAT summary you see Payment of this period's VAT. If the client tells you he has not paid, press Not paid and say why: Scribe 2.0 then books no payment and the debt stays on the VAT account. When he pays, press Paid on… and type the date (like 12.3.2026). Back to the due date undoes either. Every change keeps who, when and why.
9.4 Reports
P&L, Balance Sheet and General Ledger show the report on screen for the period you choose with From and To (the Balance Sheet uses At the end of). Language switches between Suomi and English, Full layout shows the zero lines too, and By month (P&L) puts each month in its own column. ⬇ Download PDF saves the report as it is shown. VAT Return shows the month's VAT return and has the same ✓ Close VAT period button as Posted entries.
Generate report makes several report PDFs at once: choose the from and to months and the language, tick All reports (one PDF) or single reports (P&L, P&L by month, Balance Sheet, Journal, General Ledger, VAT Report), and press ⬇ Download PDF.
9.5 Mileage Log
Reads the client's Driver's Log for the year and shows, month by month, the kilometres driven, the allowance in euros and whether it is already posted. Rate €/km is the year's tax-free rate (section 5); change it only if the rate in force differs. Post km allowance… opens a ready voucher (Dr 7870 / Cr 2365) dated the last day of the month; check it and press Confirm & Post. If the log shows that the car is used more than half for business, tick the box confirming the car is not business property before posting (section 4).
9.6 Generate invoice
Makes a sales invoice (lasku) for the client. Fill in the sender details once (they are remembered), pick or type the recipient, add the invoice rows with + Add row, and press Create invoice →. Scribe 2.0 gives it the next invoice number. 🖨 Print / Save PDF prints it or saves it as a PDF, and Suomi / English switches its language. Creating an invoice does not book it: the invoice is booked from the client's documents like any other sale.
9.7 Import Journal
Brings a Netvisor journal (PDF, up to 2.5 MB) into Scribe 2.0. Choose file shows the company and every month in the file, and says which months can be imported, which would be replaced and which cannot be imported and why. Tick the months (all / none) and press Import Journal. Importing the same month again replaces it; it never adds a second copy.
9.8 Opening balances
Opening balances lists every balance-sheet account with its 1 January balance. Type or correct the debit or credit amounts and press Save opening balances; debits and credits must be equal. Import opening balances shows last year's closing figures; Carry <year> forward into <year> makes them this year's opening balances. Afterwards, check that the Balance Sheet balances.
9.9 Chart of Accounts
Which account and VAT rate to use for each kind of cost or income, with worked examples. Search with the box, or pick a group with the chips (Most common, Income, Vehicle, Pension/admin/bank, Loans and EU commission, Private/non-deductible, Assets and VAT, Subcontractor). Full chart shows every account of the open client.
9.10 Clientele
The client's details: number, name, Business ID, company form, status, VAT period, bookkeeper and the history of changes. Save changes stores edits to the name, phone, address, accounting tool and km rate. Change VAT period records a new VAT period from a start month, with Vero's approval reference. Correct company form fixes a toiminimi/Oy mistake (give the reason). Make dormant marks a client who has paused; the banner then shows on every screen.
9.11 HR Engine
Opens right inside its tab, for the owner and the HR manager only. Every action shows a preview first and changes nothing until you confirm.
Onboard Client — creates the client, opens its first financial year, makes its folders and shares them.
Reassign Clients — moves clients to another bookkeeper from a date.
Terminate Clients / Reactivate Clients — ends or restarts a client relationship.
Edit Client — corrects a client's details.
Hire Bookkeeper / Terminate Bookkeeper — adds or removes a member of staff.
Admins — who has access to the management folders.
Check — shows whether a client is set up completely.
10. + New voucher, step by step
A manual entry gets a MAN- reference, so Smart Post never overwrites it. There is no preview button: a live preview of the exact lines appears under the form as you type (Guided shows "The tool will post:"). Read it before you press Confirm & Post. You can choose four types: Guided voucher, EU reverse-charge, Other (manual journal) and Loan. Income and Expense only appear when you fix an existing entry.
Fields every type has
Field
What to enter
Description
What the entry is, e.g. "Wolt week 3 payout". Always fill it.
Invoice ref (optional)
The invoice or receipt number.
Date (optional — defaults to month end)
The document's date. It must be inside the month you have open: a date outside that month is silently changed to the month's last day.
Paid through the business bank this month
Only on EU reverse-charge and Loan → Monthly instalment. Tick it only if the money left the business bank (counterpart 1910). For a toiminimi, leave it empty (counterpart 2365).
Amounts: numbers with two decimals, a dot for decimals (a comma works only in some browsers), no € sign. There is no attachment upload; the document stays in the client's folder, so write its number in Invoice ref.
A. Guided voucher (the default; use it for ordinary sales and costs)
Choose Amounts are: Incl. VAT (Gross) if you copy the receipt total, Excl. VAT (Net) if you copy the net.
On each line, type the Account code or name and pick it from the list. Its usual VAT rate fills in by itself.
Type the Amount. Check the VAT rate: 25.5%, 13.5%, 10%, 0%, 24% (legacy, old documents only) or EU reverse-charge.
Description on the line is optional; blank uses the voucher's Description, or the account name if that is empty too.
Enter only income and cost accounts. Never type 2939, 1763, 1764, 4130, 2365, 2361, 1910, 1920 or 1701 on a line (the tool refuses them): the tool adds the VAT rows and the counterpart itself.
Counterpart account is prefilled: 2365 for a toiminimi, 1910 for an Oy. Change it only for a real reason.
Check the preview: a sales account (3xxx) gives Cr net + Cr 2939; any other gives Dr net + Dr 1763; then one counterpart line. It must say balanced ✓ with no ⚠ warnings. Then Confirm & Post.
B. EU reverse-charge (commission charged by a company in another EU country, e.g. Bolt or Uber; not Wolt, which is Finnish)
Commission account: type the code, normally 4110. The box starts empty; if you leave it empty, 4110 is used.
Commission amount (net): the commission without VAT.
The VAT is always 25.5%. The preview shows Dr commission + Dr 1764 / Cr 4130 + Cr counterpart; the two VAT lines cancel each other.
Bank box: see the shared fields. Then Confirm & Post.
C. Other (manual journal) (anything the other types cannot do)
On each line: Account, a description, Debit or Credit, and a VAT rate if the line carries VAT.
You write every line yourself, including VAT (1763 usually Debit; 2939 and 4130 usually Credit) and the counterpart.
The running total must read balanced ✓. If it does not, the entry is refused when you press Confirm. The check lets through a difference of up to 0,02 €, so make sure the total is exactly zero. You need at least two lines, and a line cannot have both a debit and a credit.
D. Loan
New loan / financed purchase (the day the vehicle or machine is bought on finance):
Asset account: 1160 for a vehicle (left blank, 1160 is used).
Asset cost (excl. VAT) and VAT amount from the purchase invoice. For a passenger car whose VAT is not deductible (section 4), put the full price in cost and 0 in VAT.
Down payment (0 if fully financed).
Loan amount (auto) fills itself: cost + VAT − down payment. It must match the finance agreement; if not, stop and ask.
Confirm & Post, then create the schedule in Loans (section 11) and add the asset to Assets.
Monthly instalment: Principal, Interest and Fee (optional) from the lender's statement. Use it only for a loan that is not in the Loans register. For a loan in the register, Scribe 2.0 books every instalment's repayment and interest by itself on its due date, from the payment plan (section 11); booking it here as well would book it twice.
11. Assets and Loans, step by step
Assets. The register lists each asset with its value on 1 January, the year's depreciation and its value at the year end, and below them any assets sold or scrapped. Scribe 2.0 puts the depreciation into the books by itself, month by month (or once a year, see Cadence). Never post depreciation by hand.
Press Add an asset (or Edit on an existing one) and fill in:
Field
What to enter
Description
A clear name, e.g. "Toyota Corolla ABC-123"
Class
The asset group. It sets the depreciation method and the yearly rate (section 6)
Cost €
The purchase price without deductible VAT
In service
The date the asset was taken into use
Useful life (years)
Only for classes depreciated in equal parts over a number of years
Rate override
Only when a lower rate than the class's is used (section 6); otherwise leave empty
Cadence
Monthly (1/12 each month from the month taken into use) or annual (the whole year on 31.12). Empty uses the class's choice
Professional traffic
Tick only for a vehicle used in professional transport (section 4)
Disposal date, Disposal kind, Proceeds €, Traded for asset #
Only when the asset is sold, scrapped or traded in: the date, how it left, the price received and, for a trade-in, the asset that replaced it
Note
Anything worth knowing later
Then press Create the asset (or Save changes). Remove deletes an asset entered by mistake. If Scribe 2.0 says the asset can no longer be removed because its depreciation is already in the books, record a disposal instead.
Loans. First book the purchase with + New voucher → Loan → New loan / financed purchase (section 10). Then press Add a loan and copy these from the loan agreement:
Field
What to enter
Lender
The finance company, e.g. Nordea Rahoitus
Description
What was financed, e.g. the registration number
Borrowed €
The amount financed
Drawdown date
The day the loan started
The plan states
Even principal if the plan repays the same amount every month, Fixed payment (annuity) if the monthly payment stays the same
Day count
The interest convention the agreement uses
Liability account, Current portion, Interest account, Bank account
Normally 2400, 2500 (optional), 9100 and 1910 — change only for a real reason
Rate series
One line per rate: the date it starts, the yearly rate (0.0500 = 5 %) and where it comes from
Payment plan
One line per instalment: its number, due date, the amount the plan states and, if given, the bank's interest
Press Save in Scribe 2.0. From then on Scribe 2.0 books every instalment's repayment and interest by itself on its due date. Do not book the instalments again with + New voucher. Remove deletes a loan entered by mistake; Scribe 2.0 tells you if it can no longer be removed because its instalments are already in the books.
12. Prepaid tax (ennakkovero)
A toiminimi pays its income tax in advance in instalments that Vero sets in a prepayment decision (ennakkoveropäätös), which comes with a payment plan. The instalments are the owner's private tax, so in the books each one is a private withdrawal: Dr 9810 Income taxes / Cr 2365 Private withdrawals, on the instalment's due date. Scribe 2.0 does this by itself from the decision you type in; you never post it.
Where: menu → Client → Prepaid tax. It shows the year that is open in Year.
When a decision arrives (the first one of the year, or a revised one):
Under Add a prepayment decision, type the Decision date as printed on the decision, e.g. 10.12.2025.
Business share %: 100 when all of the owner's income comes from the business. If the decision also covers other income (wages, pension, rent), use the business share the decision shows for business income.
Note: the decision number, e.g. P123456789.
Instalments: one line per instalment, the due date and the amount, e.g. 23.2.2026 400,00. Copy every line of the payment plan.
Press Save decision. The instalments appear in the table at the top, each on its due date.
A revised decision replaces the earlier one from its own date onwards; instalments due before it stay as they were. Do not delete or retype the old one.
Withdraw removes a decision that was typed by mistake (you give the reason). Then type the correct one.
Back tax or a refund. When the final tax decision (verotuspäätös) of an earlier year shows back tax to pay or a refund, add it under Add an adjustment: the date of the decision, the amount (back tax as a plus amount, a refund as a minus amount, e.g. -120,00) and what it is, e.g. "2025 back tax, tax decision of 6.10.2026".
Watch out:
Never book a payment to Vero onto 9810 yourself, and do not let Smart Post book the client's tax payment receipt as a cost: the instalment is already in the books, so it would count twice.
Prepaid tax is only for a toiminimi. An Oy's income tax is booked differently.
A payment arrangement for old tax debt (maksujärjestely) is not a prepayment decision: never type it here.
Check the table against the decision after saving: the dates and amounts must match line by line.