From 458404ba7aae96d77169a3946f9913789ffc1f46 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Fri, 29 May 2026 09:56:26 +0700 Subject: [PATCH] feat: implement direct POS order model data extraction to ensure accurate receipt printing in basic_receipt mode --- static/src/js/pos_receipt_printer.js | 145 ++++++++++++++++++++++++++- 1 file changed, 140 insertions(+), 5 deletions(-) diff --git a/static/src/js/pos_receipt_printer.js b/static/src/js/pos_receipt_printer.js index b9d06de..3032dab 100755 --- a/static/src/js/pos_receipt_printer.js +++ b/static/src/js/pos_receipt_printer.js @@ -52,9 +52,27 @@ const originalPrintHtml = PosPrinterService.prototype.printHtml; patch(PosPrinterService.prototype, { setup(env) { this.env = env; + this._currentPrintOrder = null; // Store current order for data extraction super.setup(...arguments); }, + /** + * Override print() to capture the order object before HTML rendering. + * This gives us access to real model data regardless of basic_receipt mode. + */ + async print(component, props, options) { + // Capture the order object from props before rendering to HTML + if (props && props.order) { + this._currentPrintOrder = props.order; + console.log('[BluetoothPrint] print() called, captured order:', props.order?.pos_reference); + } else { + this._currentPrintOrder = null; + } + // Call the parent chain (PosPrinterService.print → PrinterService.print) + // which renders the component to HTML then calls this.printHtml(el) + return await super.print(...arguments); + }, + /** * Override the printHtml method to use bluetooth printer * Falls back to browser print on any failure @@ -438,10 +456,17 @@ patch(PosPrinterService.prototype, { console.log('[BluetoothPrint] Setting character set on generator:', escposGenerator.characterSet); } - // Parse receipt data from HTML element - console.log('[BluetoothPrint] Parsing receipt data from HTML...'); - const receiptData = this._parseReceiptDataFromHtml(el); - console.log('[BluetoothPrint] Parsed receipt data:', JSON.stringify(receiptData, null, 2)); + // Prefer direct model data over HTML parsing — HTML parsing fails in basic_receipt mode + // because prices and totals are not rendered in the DOM when basic_receipt=true. + let receiptData; + if (this._currentPrintOrder) { + console.log('[BluetoothPrint] Building receipt data from POS order model (accurate)...'); + receiptData = this._buildReceiptDataFromOrder(this._currentPrintOrder); + } else { + console.log('[BluetoothPrint] No order captured, falling back to HTML parsing...'); + receiptData = this._parseReceiptDataFromHtml(el); + } + console.log('[BluetoothPrint] Receipt data:', JSON.stringify(receiptData, null, 2)); // Generate ESC/POS commands console.log('[BluetoothPrint] Generating ESC/POS text commands...'); @@ -594,9 +619,119 @@ patch(PosPrinterService.prototype, { } }, + /** + * Build receipt data directly from the POS order model. + * This is the preferred method — it bypasses HTML parsing entirely, giving + * correct prices even when basic_receipt=true (which suppresses price DOM nodes). + * + * @private + * @param {Object} order - pos.order model instance + * @returns {Object} Structured receipt data compatible with generateReceipt() + */ + _buildReceiptDataFromOrder(order) { + console.log('[BluetoothPrint] _buildReceiptDataFromOrder called for:', order?.pos_reference); + + const pos = this.env?.services?.pos; + const company = order.company || pos?.company || {}; + const config = order.config || pos?.config || {}; + + // ── Header ───────────────────────────────────────────────────────────── + const headerData = { + companyName: company.name || config.name || 'Receipt', + address: [company.street, company.city, company.zip].filter(Boolean).join(', '), + phone: company.phone || '', + taxId: company.vat || '' + }; + + // ── Order info ───────────────────────────────────────────────────────── + let dateStr = ''; + try { + if (order.date_order) { + const d = order.date_order; + // date_order can be a luxon DateTime or a JS Date + dateStr = typeof d.toFormat === 'function' + ? d.toFormat('MM/dd/yyyy, hh:mm:ss a') + : new Date(d).toLocaleString(); + } + } catch (_) { dateStr = new Date().toLocaleString(); } + + const orderData = { + orderName: order.pos_reference || order.name || '', + date: dateStr || new Date().toLocaleString(), + cashier: (typeof order.getCashierName === 'function' ? order.getCashierName() : '') || '', + customer: order.partner_id?.name || null + }; + + // ── Order lines ──────────────────────────────────────────────────────── + const orderlines = typeof order.getOrderlines === 'function' + ? order.getOrderlines() + : (order.lines || []); + + const lines = orderlines + .filter(line => !line.combo_parent_id) // skip combo sub-lines + .map(line => { + const qty = line.qty || 0; + + // displayPrice = line total (qty * unit_price after discount, incl/excl tax per config) + const lineTotal = typeof line.displayPrice === 'number' ? line.displayPrice : 0; + + // displayPriceUnit = unit price (1 unit, incl/excl tax per config) + const unitPrice = typeof line.displayPriceUnit === 'number' + ? line.displayPriceUnit + : (qty !== 0 ? lineTotal / qty : line.price_unit || 0); + + return { + productName: line.full_product_name || line.product_id?.display_name || '', + quantity: qty, + price: unitPrice, + total: lineTotal + }; + }) + .filter(l => l.productName); + + // ── Totals ───────────────────────────────────────────────────────────── + // priceIncl = grand total including taxes + // priceExcl = grand total excluding taxes + // amountTaxes = total tax amount + const total = typeof order.priceIncl === 'number' ? order.priceIncl : 0; + const subtotal = typeof order.priceExcl === 'number' ? order.priceExcl : total; + const tax = typeof order.amountTaxes === 'number' ? order.amountTaxes : 0; + + const totals = { subtotal, tax, discount: 0, total }; + + // ── Payment ──────────────────────────────────────────────────────────── + const paymentLines = order.payment_ids || []; + let paymentMethod = 'Cash'; + let paymentAmount = total; + + if (paymentLines.length > 0) { + paymentMethod = paymentLines + .map(p => p.payment_method_id?.name || 'Cash') + .join(', '); + paymentAmount = typeof order.amountPaid === 'number' ? order.amountPaid : total; + } + + const change = typeof order.change === 'number' ? Math.max(0, order.change) : 0; + + const paymentData = { method: paymentMethod, amount: paymentAmount, change }; + + // ── Footer ───────────────────────────────────────────────────────────── + const footerData = { + message: config.receipt_footer || 'Thank you for your business!', + barcode: orderData.orderName || null + }; + + const receiptData = { headerData, orderData, lines, totals, paymentData, footerData }; + console.log('[BluetoothPrint] Built receipt data from model:', receiptData); + return receiptData; + }, + /** * Parse receipt data from HTML element for ESC/POS generation - * + * FALLBACK ONLY — used when order model is not available. + * NOTE: In basic_receipt mode Odoo does NOT render price/total DOM nodes, + * so values parsed from HTML will be 0. Always prefer _buildReceiptDataFromOrder. + * * @private * @param {HTMLElement} el - Receipt HTML element * @returns {Object} Structured receipt data