From e37b6a89111690bd38ea4578e819062b16f8a843 Mon Sep 17 00:00:00 2001 From: Sridhar Bala Date: Fri, 16 Oct 2026 08:48:55 +0530 Subject: [PATCH 1/5] Fix system default receipt printer resolution --- src/hardware-manager.js | 11 +++++++-- src/windows-spooler.js | 20 +++++++++++++++- tests/default-printer.test.js | 40 +++++++++++++++++++++++++++++++ tests/local-billing-print.test.js | 14 +++++++++++ 4 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 tests/default-printer.test.js diff --git a/src/hardware-manager.js b/src/hardware-manager.js index 36d630da9..cb8eb0e58 100644 --- a/src/hardware-manager.js +++ b/src/hardware-manager.js @@ -561,7 +561,8 @@ class HardwareManager { // A text-only thermal driver may accept HTML/GDI and still feed blank paper. // Render the complete design before submitting; never fall back after RAW acceptance. if (options.thermalRaster === true && ['58mm', '80mm', '112mm'].includes(options.pageSize)) { - const deviceName = await this._resolvePrinterName(options.printerName); + const deviceName = await this._resolvePrinterName(options.printerName) || (await this.getDefaultPrinter())?.name; + if (!deviceName) throw new Error('No default printer is available. Choose a sales receipt printer in Printer Settings.'); receiptPrinter = deviceName; const bytes = options.fastReceiptLines ? require('./receipt-fast').renderFastReceipt(options.fastReceiptLines, options.pageSize.replace('mm', '')) @@ -740,13 +741,19 @@ class HardwareManager { async getDefaultPrinter() { try { const printers = await this.listPrinters(); + // Electron can omit isDefault on Windows. Enumeration order is not a + // default: its first entry may be an offline or kitchen printer. + if (process.platform === 'win32') { + const name = await spooler.defaultPrinterName(); + return name ? printers.find(p => p.name === name) || { name, displayName: name, isDefault: true } : null; + } const defaultPrinter = printers.find(p => p.isDefault); if (defaultPrinter) { console.log('Default printer:', defaultPrinter.name); return defaultPrinter; } console.warn('No default printer found'); - return printers.length > 0 ? printers[0] : null; + return null; } catch (error) { console.error('Failed to get default printer:', error); return null; diff --git a/src/windows-spooler.js b/src/windows-spooler.js index bdd1845b3..d05b8b4aa 100644 --- a/src/windows-spooler.js +++ b/src/windows-spooler.js @@ -3,6 +3,24 @@ const { execFile } = require('child_process'); const crypto = require('crypto'); +async function defaultPrinterName(run = execFile) { + const script = `[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false) +$ErrorActionPreference = 'Stop' +@(Get-CimInstance Win32_Printer | Where-Object { $_.Default } | Select-Object -ExpandProperty Name) | ConvertTo-Json -Compress`; + return new Promise((resolve, reject) => run('powershell.exe', + ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(script, 'utf16le').toString('base64')], + { windowsHide: true, timeout: 5000, maxBuffer: 128 * 1024, encoding: 'utf8' }, + (error, stdout) => { + if (error) return reject(new Error('Could not read the Windows default printer')); + try { + const text = stdout.replace(/^\uFEFF/, '').trim(); + const value = text ? JSON.parse(text) : []; + const names = Array.isArray(value) ? value : [value]; + resolve(names.length === 1 && typeof names[0] === 'string' ? names[0] : null); + } catch (_) { reject(new Error('Invalid Windows default printer response')); } + })); +} + function result(printerName, values = {}) { return { printerName, jobId: null, state: 'failed', submitted: false, error: '', retryable: true, success: false, ...values }; @@ -103,5 +121,5 @@ function createSpooler({ inspect = inspectWindows, platform = process.platform, } const service = createSpooler(); -module.exports = { ...service, createSpooler, result, +module.exports = { ...service, createSpooler, result, defaultPrinterName, documentName: (key, index) => 'Posnic-KOT-' + crypto.createHash('sha256').update(`${key}:${index}`).digest('hex').slice(0, 32) }; diff --git a/tests/default-printer.test.js b/tests/default-printer.test.js new file mode 100644 index 000000000..d4f3e15aa --- /dev/null +++ b/tests/default-printer.test.js @@ -0,0 +1,40 @@ +'use strict'; +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const vm = require('node:vm'); +const { defaultPrinterName } = require('../src/windows-spooler'); +const source = fs.readFileSync(require.resolve('../src/hardware-manager'), 'utf8'); +const start = source.indexOf(' async getDefaultPrinter()'); +const end = source.indexOf('\n // ', start); +function resolveDefault(platform, lookup) { + return vm.runInNewContext('({' + source.slice(start, end) + '}).getDefaultPrinter', { + process: { platform }, spooler: { defaultPrinterName: lookup }, console, + }); +} +test('Windows default is used when Electron omits its default flag and lists an offline printer first', async () => { + const printers = [{ name: 'Offline Xprinter' }, { name: 'Reception' }]; + assert.equal((await resolveDefault('win32', async () => 'Reception').call({ listPrinters: async () => printers })).name, 'Reception'); +}); +test('missing or unreadable Windows default never sends a receipt to the first enumerated printer', async () => { + for (const lookup of [async () => null, async () => { throw Error('Unavailable'); }]) { + assert.equal(await resolveDefault('win32', lookup).call({ listPrinters: async () => [{ name: 'Kitchen' }] }), null); + } +}); +test('non-Windows defaults remain explicit and never depend on enumeration order', async () => { + for (const platform of ['darwin', 'linux']) { + const method = resolveDefault(platform, () => { throw Error('Windows must not be queried'); }); + assert.equal((await method.call({ listPrinters: async () => [{ name: 'Other' }, { name: 'Counter', isDefault: true }] })).name, 'Counter'); + assert.equal(await method.call({ listPrinters: async () => [{ name: 'Other' }] }), null); + } +}); +test('Windows default query is bounded, hidden, Unicode-safe and handles no default', async () => { + for (const [output, expected] of [['"Réception தமிழ்"', 'Réception தமிழ்'], ['', null], ['[]', null], ['["One","Two"]', null]]) { + assert.equal(await defaultPrinterName((file, args, options, done) => { + assert.equal(file, 'powershell.exe'); assert.equal(options.windowsHide, true); + assert.equal(options.timeout, 5000); + assert.match(Buffer.from(args.at(-1), 'base64').toString('utf16le'), /Win32_Printer/); + done(null, output); + }), expected); + } +}); diff --git a/tests/local-billing-print.test.js b/tests/local-billing-print.test.js index 7d81ecdc2..768ee37f5 100644 --- a/tests/local-billing-print.test.js +++ b/tests/local-billing-print.test.js @@ -47,6 +47,20 @@ test('designed thermal copies use RAW once each, with distinct delivery identiti assert.notEqual(calls[0][3].jobId, calls[1][3].jobId); }); +test('thermal System Default resolves to the actual default before RAW submission', async () => { + const calls = []; + const method = printMethod(async () => Buffer.from('receipt')); + const ctx = { _resolvePrinterName: async () => '', getDefaultPrinter: async () => ({ name: 'Reception' }), + sendRawToPrinter: async name => { calls.push(name); return { success: true }; } }; + assert.equal((await method.call(ctx, '', { thermalRaster: true, pageSize: '80mm' })).success, true); + assert.deepEqual(calls, ['Reception']); + ctx.getDefaultPrinter = async () => null; + const failed = await method.call(ctx, '', { thermalRaster: true, pageSize: '80mm' }); + assert.equal(failed.success, false); + assert.match(failed.error, /Choose a sales receipt printer/); + assert.equal(calls.length, 1); +}); + test('capture-only renders the actual receipt but submits no copies and reports no delivery', async () => { const { DiagnosticSession } = require('../src/diagnostic-session'); const session = new DiagnosticSession(); session.start(); session.setCapture(true); From e7da6357c4bbc73c7f51e52004c41319039f0cc1 Mon Sep 17 00:00:00 2001 From: Sridhar Bala Date: Fri, 16 Oct 2026 09:00:26 +0530 Subject: [PATCH 2/5] Allow custom receipt footer text and omit placeholder addresses --- api/src/helpers/receipt-design.js | 5 ++ .../static/script/js/core/receipt-designer.js | 8 +++- .../js/modules/js/receipt-designer-editor.js | 5 +- tests/receipt-designer.test.js | 46 +++++++++++++++++++ 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/api/src/helpers/receipt-design.js b/api/src/helpers/receipt-design.js index a4cb19662..346337ef0 100644 --- a/api/src/helpers/receipt-design.js +++ b/api/src/helpers/receipt-design.js @@ -120,6 +120,11 @@ showBranding: value.showBranding !== false, layouts: {}, }; + if (value.brandingText != null) { + if (typeof value.brandingText !== 'string' || value.brandingText.length > 200) + throw new Error('Receipt footer text must be at most 200 characters.'); + out.brandingText = value.brandingText.trim(); + } Object.keys(formats).forEach(function (format) { const layout = layoutFor(value, format); if (!layout || !Array.isArray(layout.blocks) || layout.blocks.length > 40) diff --git a/frontend/static/script/js/core/receipt-designer.js b/frontend/static/script/js/core/receipt-designer.js index 55153e62a..7a753e000 100644 --- a/frontend/static/script/js/core/receipt-designer.js +++ b/frontend/static/script/js/core/receipt-designer.js @@ -231,7 +231,11 @@ if (b.type === 'store') { function storeText(key, fallback) { return String(b[key] || '').trim() || fallback || ''; } var storeName = storeText('nameText', data.branch_name || data.store_name || PosnicPro.local.get('branchname')); - var storeAddress = storeText('addressText', plain(data.printing_address || data.store_address || '')); + function addressText(value) { + var text = plain(value || '').trim(); + return /^(not provided|not available|not set|n\/a|null|undefined)$/i.test(text) ? '' : text; + } + var storeAddress = addressText(b.addressText) || addressText(data.printing_address) || addressText(data.store_address); var storePhone = storeText('phoneText', data.store_telephone); var storeEmail = storeText('emailText', data.store_email); var storeRegistration = storeText('registrationText', data.branch_gstin_number); @@ -350,7 +354,7 @@ }).filter(Boolean); rendered = pairFields(rendered); var html = sheet ? composeSheet(rendered) : rendered.map(function (b) { return b.html; }).join(''); - if (designs.showBranding !== false) html += '
Made with Posnic · posnic.com
'; + if (designs.showBranding !== false) html += '
' + esc(String(designs.brandingText || '').trim() || 'Made with Posnic · posnic.com') + '
'; if (pendingGoods) html += '
' + esc(label('Payment pending')) + '
'; if (sheet) { var notes = ''; diff --git a/frontend/static/script/js/modules/js/receipt-designer-editor.js b/frontend/static/script/js/modules/js/receipt-designer-editor.js index 600ec56d7..781a65efe 100644 --- a/frontend/static/script/js/modules/js/receipt-designer-editor.js +++ b/frontend/static/script/js/modules/js/receipt-designer-editor.js @@ -232,6 +232,7 @@ renderFormats(); box.find('#rd-text-size').val(layout().fontSize); box.find('#rd-branding').prop('checked', design.showBranding !== false); + box.find('#rd-branding-text').val(design.brandingText || ''); renderList(); preview(); } function move(id, at) { @@ -331,7 +332,7 @@ '
' + '

' + esc(t('Your layout')) + '

' + button('reset-template', 'Reset template', 'refresh-cw') + button('undo', 'Undo', 'rotate-ccw', 'disabled') + '
' + esc(t('Applies to this paper format. Select a block to override its text size or make it bold.')) + '

    ' + esc(t('Store details, receipt details, items and totals are always included.')) + '

    ' + '
    ' + - '
    '); + '
    ' + esc(t('Leave blank to use the default. Applies to all paper sizes.')) + '
    '); if (sampleTaxEnabled()) { box.find('.rd-preview-footer').append(''); box.find('#rd-sample-tax').val(sampleInterstate ? 'inter' : 'intra').on('change', function () { sampleInterstate = this.value === 'inter'; schedulePreview(); }); @@ -357,6 +358,8 @@ format = this.getAttribute('data-format'); selected = null; box.find('.rd-preview-stage').scrollTop(0); renderEditor(); }).on('change.receiptDesigner', '#rd-branding', function () { checkpoint(); design.showBranding = this.checked; changed(); + }).on('input.receiptDesigner', '#rd-branding-text', function () { + checkpoint(); design.brandingText = this.value; changed(); }).on('click.receiptDesigner', '[data-default-format]', function () { var next = this.getAttribute('data-default-format'); if (next === design.defaultFormat) return; diff --git a/tests/receipt-designer.test.js b/tests/receipt-designer.test.js index 08fe4142e..0c1cff864 100644 --- a/tests/receipt-designer.test.js +++ b/tests/receipt-designer.test.js @@ -1246,6 +1246,34 @@ test('sale printing sends receipt identity to desktop logging while sample print dom.window.close(); }); +test('custom footer text saves, reloads and renders safely on every paper size', () => { + const { dom, w, $, branch, sale, engine } = setup(); + let sent; + w.PosnicPro.put = (request, done) => { sent = JSON.parse(request.data); done({ type: 'success', data: { receipt_designs: sent.receipt_designs } }); }; + w.PosnicPro.receiptDesignerEditor.load(branch); + const custom = 'Printed by · example.com'; + $('#rd-branding-text').val(custom).trigger('input'); + $('[data-action="save"]').trigger('click'); + const saved = contract.normalize(sent.receipt_designs); + assert.equal(saved.brandingText, custom); + w.PosnicPro.receiptDesignerEditor.load({ ...branch, receipt_designs: saved }); + assert.equal($('#rd-branding-text').val(), custom); + sale.receipt_designs = saved; + for (const format of Object.keys(contract.formats)) { + const footer = $('
    ').html(engine.render(sale, format, false)).find('.rd-branding'); + assert.equal(footer.text(), custom); + assert.equal(footer.children().length, 0); + } + saved.showBranding = false; + assert.equal($('
    ').html(engine.render(sale, '80', false)).find('.rd-branding').length, 0); + saved.showBranding = true; + saved.brandingText = ' '; + assert.equal($('
    ').html(engine.render(sale, '80', false)).find('.rd-branding').text(), 'Made with Posnic · posnic.com'); + assert.throws(() => contract.normalize({ ...saved, brandingText: 'x'.repeat(201) }), /200 characters/); + assert.throws(() => contract.normalize({ ...saved, brandingText: {} }), /200 characters/); + dom.window.close(); +}); + test('store print overrides persist per format without changing branch contact data', () => { const { dom, w, $, branch, sale, engine } = setup(); branch.store_telephone = '+919876543210'; @@ -1271,6 +1299,24 @@ test('store print overrides persist per format without changing branch contact d dom.window.close(); }); +test('missing branch addresses never print placeholder text in any receipt format', () => { + const { dom, $, engine, sale, design } = setup(); + for (const format of Object.keys(contract.formats)) { + const store = design.layouts[format].blocks.find(b => b.type === 'store'); + Object.assign(sale, { printing_address: ' Not provided ', store_address: '' }); + sale.receipt_designs = design; + const address = () => $('
    ').html(engine.render(sale, format, false)).find('.rd-store-contact').text(); + assert.equal(address(), ''); + sale.store_address = '12 Market Road'; + assert.equal(address(), '12 Market Road'); + store.addressText = 'Receipt address'; + assert.equal(address(), 'Receipt address'); + store.addressText = 'Not provided'; + assert.equal(address(), '12 Market Road'); + } + dom.window.close(); +}); + test('store visibility, empty overrides, escaping and validation apply to every format', () => { const { dom, $, engine, sale, design } = setup(); sale.store_telephone = '+919876543210'; From 53c4fb85fdc26585a301104e943e95b80fd41e40 Mon Sep 17 00:00:00 2001 From: Sridhar Bala Date: Fri, 16 Oct 2026 09:27:12 +0530 Subject: [PATCH 3/5] Hide zero receipt taxes and align sample tax labels with country --- api/src/helpers/receipt-design.js | 24 ++++++++++++++ .../static/script/js/core/receipt-designer.js | 11 ++++--- .../js/modules/js/receipt-designer-editor.js | 7 ++-- tests/receipt-designer.test.js | 32 +++++++++++++++++++ 4 files changed, 67 insertions(+), 7 deletions(-) diff --git a/api/src/helpers/receipt-design.js b/api/src/helpers/receipt-design.js index 346337ef0..de51ef159 100644 --- a/api/src/helpers/receipt-design.js +++ b/api/src/helpers/receipt-design.js @@ -5,6 +5,29 @@ else root.PosnicReceiptDesign = factory(); })(typeof window !== 'undefined' ? window : this, function () { 'use strict'; + function taxLabel(data, fallbackCountry) { + data = data || {}; + const country = String( + data.branch_country || data.country || data.sortname || fallbackCountry || '' + ) + .trim() + .toLowerCase(); + if (['in', 'ind', 'india', 'au', 'aus', 'australia'].includes(country)) return 'GST'; + if ( + [ + 'gb', + 'gbr', + 'uk', + 'united kingdom', + 'united kingdom of great britain and northern ireland', + 'za', + 'zaf', + 'south africa', + ].includes(country) + ) + return 'VAT'; + return 'Tax'; + } function taxRegistrationLabel(data, fallbackCountry) { data = data || {}; const country = String( @@ -272,6 +295,7 @@ return out; } return { + taxLabel: taxLabel, taxRegistrationLabel: taxRegistrationLabel, formats: formats, fields: fields, diff --git a/frontend/static/script/js/core/receipt-designer.js b/frontend/static/script/js/core/receipt-designer.js index 7a753e000..561c2e31c 100644 --- a/frontend/static/script/js/core/receipt-designer.js +++ b/frontend/static/script/js/core/receipt-designer.js @@ -300,13 +300,14 @@ var igst = items.reduce(function (n, i) { return n + Number(i.igst_tax || 0); }, 0); var cgst = items.reduce(function (n, i) { return n + Number(i.cgst_tax || 0); }, 0); var sgst = items.reduce(function (n, i) { return n + Number(i.sgst_tax == null ? i.cgst_tax || 0 : i.sgst_tax); }, 0); - if (Array.isArray(data.receipt_tax_rows) && data.receipt_tax_rows.length) data.receipt_tax_rows.forEach(function (row) { content += pair(row.label, money(row.amount)); }); + var hasTaxAmount = function (amount) { return Number.isFinite(Number(amount)) && Number(Number(amount).toFixed(2)) !== 0; }; + if (Array.isArray(data.receipt_tax_rows) && data.receipt_tax_rows.length) data.receipt_tax_rows.forEach(function (row) { if (hasTaxAmount(row.amount)) content += pair(row.label, money(row.amount)); }); else if (igst || cgst || sgst) { - if (igst) content += pair('IGST', money(igst)); - if (cgst) content += pair('CGST', money(cgst)); - if (sgst) content += pair('SGST', money(sgst)); + if (hasTaxAmount(igst)) content += pair('IGST', money(igst)); + if (hasTaxAmount(cgst)) content += pair('CGST', money(cgst)); + if (hasTaxAmount(sgst)) content += pair('SGST', money(sgst)); } - else if (Number(data.tax)) content += pair('Tax', money(data.tax)); + else if (hasTaxAmount(data.tax)) content += pair(contract.taxLabel(data, PosnicPro.local.get('country_setting')), money(data.tax)); (data.charges || []).forEach(function (c) { content += pair(c.name || 'Charge', money(Number(c.amount || 0) + Number(c.tax_amount || 0))); }); if (Number(data.round_off)) content += pair('Round Off', money(data.round_off)); content += pair('Total', money(data.items_total), true); diff --git a/frontend/static/script/js/modules/js/receipt-designer-editor.js b/frontend/static/script/js/modules/js/receipt-designer-editor.js index 781a65efe..c01c516aa 100644 --- a/frontend/static/script/js/modules/js/receipt-designer-editor.js +++ b/frontend/static/script/js/modules/js/receipt-designer-editor.js @@ -26,11 +26,14 @@ function changed() { status(t('Unsaved changes')); box.find('[data-action="undo"]').prop('disabled', !undo.length); schedulePreview(); } var sampleInterstate = false; function sampleTaxEnabled() { return ![false, 'false', 'disable'].includes(branch.module_tax_enable); } - function sampleIndianGst() { return ['gst_on', 'enable', true, 'true'].includes(branch.indian_gst) || ['enable', true, 'true'].includes(branch.gst); } + function sampleIndianGst() { + var country = String(branch.branch_country || branch.country || branch.sortname || PosnicPro.local.get('country_setting') || '').trim().toLowerCase(); + return country ? ['india', 'in', 'ind'].includes(country) : ['gst_on', 'enable', true, 'true'].includes(branch.indian_gst) || ['enable', true, 'true'].includes(branch.gst); + } function sample() { var item = function (name, price, qty) { return { item_name: name, item_price: price, item_quantity: qty, item_unit: 'ea', total_amount: price * qty, hsn_code: '1234' }; }; var tax = sampleTaxEnabled() ? 2.25 : 0; - var taxRows = !tax ? [] : sampleIndianGst() ? (sampleInterstate ? [{ label: t('IGST') + ' 5%', amount: tax }] : [{ label: t('CGST') + ' 2.5%', amount: 1.13 }, { label: t('SGST') + ' 2.5%', amount: 1.12 }]) : [{ label: t('Tax') + ' 5%', amount: tax }]; + var taxRows = !tax ? [] : sampleIndianGst() ? (sampleInterstate ? [{ label: t('IGST') + ' 5%', amount: tax }] : [{ label: t('CGST') + ' 2.5%', amount: 1.13 }, { label: t('SGST') + ' 2.5%', amount: 1.12 }]) : [{ label: t(schema.taxLabel(branch, PosnicPro.local.get('country_setting'))) + ' 5%', amount: tax }]; return Object.assign({}, branch, printOptions(), { receipt_designs: design, sales_id: 'S-000128', created_date: new Date().toLocaleString('en-GB', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }), customer_name: 'Alex Morgan', customer_phone: '+1 202 555 0148', customer_email: 'alex@example.com', customer_address: '24 Market Street', customer_gstin: '', customer_tax_number: layout().blocks.some(function (b) { return b.field === 'customer_tax_number'; }) ? 'TAX-123456' : '', diff --git a/tests/receipt-designer.test.js b/tests/receipt-designer.test.js index 0c1cff864..fbe53cb02 100644 --- a/tests/receipt-designer.test.js +++ b/tests/receipt-designer.test.js @@ -1359,6 +1359,38 @@ test('designer samples include GST, switch to IGST, and respect tax disabled', a dom.window.close(); }); +test('zero tax rows are omitted while real small amounts and saved tax labels remain', () => { + const { dom, engine, sale, $ } = setup(); + for (const format of Object.keys(contract.formats)) { + sale.tax = 0; + sale.receipt_tax_rows = [{ label: 'VAT', amount: 0 }, { label: 'CGST', amount: '0.00' }, { label: 'SGST', amount: 0.000001 }]; + const totals = () => $('
    ').html(engine.render(sale, format, false)).find('.rd-totals').text(); + assert.doesNotMatch(totals(), /VAT|CGST|SGST/); + sale.receipt_tax_rows = [{ label: 'CGST', amount: 0.02 }, { label: 'SGST', amount: 0.01 }]; + assert.match(totals(), /CGST.*0.02.*SGST.*0.01/); + sale.receipt_tax_rows = [{ label: 'Local sales levy', amount: 2 }]; + assert.match(totals(), /Local sales levy/); + } + dom.window.close(); +}); + +test('sample tax follows country even without the old India GST flag and hides when disabled', async () => { + const { dom, w, engine, branch } = setup(); + let document; + engine.print = async html => { document = html; return { success: true }; }; + for (const [country, label] of [['India', 'CGST 2.5%'], ['South Africa', 'VAT 5%'], ['United Kingdom', 'VAT 5%'], ['Australia', 'GST 5%']]) { + Object.assign(branch, { country, indian_gst: 'gst_off', module_tax_enable: true }); + w.PosnicPro.receiptDesignerEditor.load(branch); + await w.PosnicPro.receiptDesignerEditor.printSample(); + assert.ok(document.includes(label), country); + branch.module_tax_enable = false; + w.PosnicPro.receiptDesignerEditor.load(branch); + await w.PosnicPro.receiptDesignerEditor.printSample(); + assert.ok(!document.includes(label), country); + } + dom.window.close(); +}); + test('receipt fallback preserves independently saved CGST and SGST amounts', () => { const { dom, engine, sale, $ } = setup(); sale.items[0].cgst_tax = 1.13; sale.items[0].sgst_tax = 1.12; From fce9c1ec4a858ebc3d41dbcce0942f9ec39c301f Mon Sep 17 00:00:00 2001 From: Sridhar Bala Date: Fri, 16 Oct 2026 09:34:50 +0530 Subject: [PATCH 4/5] Register receipt footer placeholder for translation --- frontend/static/script/js/modules/js/receipt-designer-editor.js | 2 +- languages/_english.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/static/script/js/modules/js/receipt-designer-editor.js b/frontend/static/script/js/modules/js/receipt-designer-editor.js index c01c516aa..11f2d9c67 100644 --- a/frontend/static/script/js/modules/js/receipt-designer-editor.js +++ b/frontend/static/script/js/modules/js/receipt-designer-editor.js @@ -335,7 +335,7 @@ '
    ' + '

    ' + esc(t('Your layout')) + '

    ' + button('reset-template', 'Reset template', 'refresh-cw') + button('undo', 'Undo', 'rotate-ccw', 'disabled') + '
    ' + esc(t('Applies to this paper format. Select a block to override its text size or make it bold.')) + '

      ' + esc(t('Store details, receipt details, items and totals are always included.')) + '

      ' + '
      ' + - '
      ' + esc(t('Leave blank to use the default. Applies to all paper sizes.')) + '
      '); + '
      ' + esc(t('Leave blank to use the default. Applies to all paper sizes.')) + '
      '); if (sampleTaxEnabled()) { box.find('.rd-preview-footer').append(''); box.find('#rd-sample-tax').val(sampleInterstate ? 'inter' : 'intra').on('change', function () { sampleInterstate = this.value === 'inter'; schedulePreview(); }); diff --git a/languages/_english.json b/languages/_english.json index 00f0fe2ec..9e5662614 100644 --- a/languages/_english.json +++ b/languages/_english.json @@ -2360,6 +2360,7 @@ "lang_m_r_p": "M.R.P", "lang_machine_host_ip_optional": "Machine host / IP (optional)", "lang_macos": "macOS", + "lang_made_with_posnic_posnic_com": "Made with Posnic · posnic.com", "lang_mail": "Report Email Settings", "lang_make_the_change": "Make the change", "lang_making_this_shop_yours": "Making this shop yours?", From f0b0765a20bfa8e1ebe89e418a44172bd061c838 Mon Sep 17 00:00:00 2001 From: Sridhar Bala Date: Fri, 16 Oct 2026 09:39:34 +0530 Subject: [PATCH 5/5] Translate receipt footer placeholder in Dutch --- languages/nl.json | 1 + 1 file changed, 1 insertion(+) diff --git a/languages/nl.json b/languages/nl.json index 468bd0586..3a3209c39 100644 --- a/languages/nl.json +++ b/languages/nl.json @@ -1,4 +1,5 @@ { + "lang_made_with_posnic_posnic_com": "Gemaakt met Posnic · posnic.com", "lang_actual_size": "Werkelijke grootte", "lang_fit_whole_page": "Hele pagina passend weergeven", "lang_select_products_on_this_page": "Producten op deze pagina selecteren",