feat: implement order and line notes for basic receipts in ESC/POS generator and update receipt data mapping

This commit is contained in:
Suherdy Yacob 2026-06-03 09:46:18 +07:00
parent e80bce30fa
commit 5524546dab
3 changed files with 51 additions and 7 deletions

View File

@ -207,6 +207,7 @@ export class EscPosGenerator {
// ── Initialize ──────────────────────────────────────────────────── // ── Initialize ────────────────────────────────────────────────────
cmds.push(...this.initialize()); cmds.push(...this.initialize());
cmds.push(...this.setAlignment('left'));
// ── Header ──────────────────────────────────────────────────────── // ── Header ────────────────────────────────────────────────────────
// On basic receipt (table checker): skip company header, print only minimal info // On basic receipt (table checker): skip company header, print only minimal info
@ -353,6 +354,25 @@ export class EscPosGenerator {
cmds.push(...this.addLine(this.divider())); cmds.push(...this.addLine(this.divider()));
} }
// ── Order Notes (Basic Receipt) ───────────────────────────────────
if (receiptData.isBasicReceipt && receiptData.orderData) {
const o = receiptData.orderData;
if (o.internalNote) {
const noteLines = String(o.internalNote).split('\n');
cmds.push(...this.addLine('Order Note:', { height: 2, bold: true }));
noteLines.forEach(l => {
cmds.push(...this.addLine(` ${l}`, { height: 2, bold: true }));
});
}
if (o.customerNote) {
const noteLines = String(o.customerNote).split('\n');
cmds.push(...this.addLine('Cust Note:', { height: 2, bold: true }));
noteLines.forEach(l => {
cmds.push(...this.addLine(` ${l}`, { height: 2, bold: true }));
});
}
}
// ── Totals ──────────────────────────────────────────────────────── // ── Totals ────────────────────────────────────────────────────────
// Hidden on basic receipt — matches t-if="!props.basic_receipt" in order_receipt.xml // Hidden on basic receipt — matches t-if="!props.basic_receipt" in order_receipt.xml
if (!receiptData.isBasicReceipt && receiptData.totals) { if (!receiptData.isBasicReceipt && receiptData.totals) {

View File

@ -248,11 +248,14 @@ patch(PosPrinterService.prototype, {
if (isMobile) { if (isMobile) {
console.log('[BluetoothPrint] Mobile or WebView environment detected. Falling back to native printWeb on main window.'); console.log('[BluetoothPrint] Mobile or WebView environment detected. Falling back to native printWeb on main window.');
try { try {
this.printWeb(el); console.log('[BluetoothPrint] Invoking await this.printWeb(el)...');
await this.printWeb(el);
console.log('[BluetoothPrint] printWeb has finished executing.');
return true; return true;
} catch (e) { } catch (e) {
console.error('[BluetoothPrint] printWeb failed, calling window.print directly:', e); console.error('[BluetoothPrint] printWeb failed, calling window.print directly:', e);
window.print(el); window.print(el);
console.log('[BluetoothPrint] direct window.print has returned.');
return true; return true;
} }
} }
@ -260,6 +263,7 @@ patch(PosPrinterService.prototype, {
return new Promise((resolve) => { return new Promise((resolve) => {
// Create a hidden iframe for printing // Create a hidden iframe for printing
const printFrame = document.createElement('iframe'); const printFrame = document.createElement('iframe');
printFrame.className = 'pos-print-iframe';
printFrame.style.position = 'fixed'; printFrame.style.position = 'fixed';
printFrame.style.right = '0'; printFrame.style.right = '0';
printFrame.style.bottom = '0'; printFrame.style.bottom = '0';
@ -655,6 +659,7 @@ patch(PosPrinterService.prototype, {
_buildReceiptDataFromOrder(order) { _buildReceiptDataFromOrder(order) {
console.log('[BluetoothPrint] _buildReceiptDataFromOrder called for:', order?.pos_reference); console.log('[BluetoothPrint] _buildReceiptDataFromOrder called for:', order?.pos_reference);
const isBasic = !!this._currentPrintBasic;
const pos = this.env?.services?.pos; const pos = this.env?.services?.pos;
const company = order.company || pos?.company || {}; const company = order.company || pos?.company || {};
const config = order.config || pos?.config || {}; const config = order.config || pos?.config || {};
@ -716,6 +721,8 @@ patch(PosPrinterService.prototype, {
cashier: formatFirstLastName((typeof order.getCashierName === 'function' ? order.getCashierName() : '') || ''), cashier: formatFirstLastName((typeof order.getCashierName === 'function' ? order.getCashierName() : '') || ''),
customer: order.partner_id?.name || null, customer: order.partner_id?.name || null,
tableName, tableName,
internalNote: isBasic ? (pos?.getStrNotes ? pos.getStrNotes(order.internal_note || '[]') : (typeof order.internal_note === 'string' ? order.internal_note : '')) : '',
customerNote: isBasic ? (order.general_customer_note || '') : '',
}; };
// ── Order lines ──────────────────────────────────────────────────────── // ── Order lines ────────────────────────────────────────────────────────
@ -723,7 +730,6 @@ patch(PosPrinterService.prototype, {
? order.getOrderlines() ? order.getOrderlines()
: (order.lines || []); : (order.lines || []);
const isBasic = !!this._currentPrintBasic;
const lines = orderlines const lines = orderlines
.filter(line => { .filter(line => {
if (line.combo_parent_id) return false; // skip combo sub-lines if (line.combo_parent_id) return false; // skip combo sub-lines
@ -747,8 +753,22 @@ patch(PosPrinterService.prototype, {
? line.displayPriceUnit ? line.displayPriceUnit
: (qty !== 0 ? lineTotal / qty : line.price_unit || 0); : (qty !== 0 ? lineTotal / qty : line.price_unit || 0);
// Customer-facing note (always shown, even on basic receipt) // Construct combined line note if basic receipt, else empty string
const note = line.customer_note || ''; let note = '';
if (isBasic) {
const noteParts = [];
const customerNote = line.customer_note || line.customerNote || '';
if (customerNote) {
noteParts.push(customerNote);
}
if (line.note) {
const internalNote = pos?.getStrNotes ? pos.getStrNotes(line.note) : (typeof line.note === 'string' ? line.note : '');
if (internalNote) {
noteParts.push(internalNote);
}
}
note = noteParts.join(' | ');
}
// Find combo sub-lines linked to this line // Find combo sub-lines linked to this line
const comboSubLines = orderlines const comboSubLines = orderlines

View File

@ -31,14 +31,17 @@ describe('ESC/POS Conversion Properties', () => {
orderName: fc.string({ minLength: 1, maxLength: 50 }), orderName: fc.string({ minLength: 1, maxLength: 50 }),
date: fc.date().map(d => d.toISOString()), date: fc.date().map(d => d.toISOString()),
cashier: fc.string({ minLength: 1, maxLength: 50 }), cashier: fc.string({ minLength: 1, maxLength: 50 }),
customer: fc.option(fc.string({ minLength: 1, maxLength: 100 })) customer: fc.option(fc.string({ minLength: 1, maxLength: 100 })),
internalNote: fc.option(fc.string({ maxLength: 100 })),
customerNote: fc.option(fc.string({ maxLength: 100 }))
}), }),
lines: fc.array( lines: fc.array(
fc.record({ fc.record({
productName: fc.string({ minLength: 1, maxLength: 100 }), productName: fc.string({ minLength: 1, maxLength: 100 }),
quantity: fc.float({ min: Math.fround(0.01), max: Math.fround(1000), noNaN: true }), quantity: fc.float({ min: Math.fround(0.01), max: Math.fround(1000), noNaN: true }),
price: fc.float({ min: Math.fround(0.01), max: Math.fround(100000), noNaN: true }), price: fc.float({ min: Math.fround(0.01), max: Math.fround(100000), noNaN: true }),
total: fc.float({ min: Math.fround(0.01), max: Math.fround(100000), noNaN: true }) total: fc.float({ min: Math.fround(0.01), max: Math.fround(100000), noNaN: true }),
note: fc.option(fc.string({ maxLength: 100 }))
}), }),
{ minLength: 1, maxLength: 50 } { minLength: 1, maxLength: 50 }
), ),
@ -56,7 +59,8 @@ describe('ESC/POS Conversion Properties', () => {
footerData: fc.record({ footerData: fc.record({
message: fc.string({ maxLength: 200 }), message: fc.string({ maxLength: 200 }),
barcode: fc.option(fc.string({ minLength: 1, maxLength: 50 })) barcode: fc.option(fc.string({ minLength: 1, maxLength: 50 }))
}) }),
isBasicReceipt: fc.boolean()
}); });
}; };