feat: implement direct POS order model data extraction to ensure accurate receipt printing in basic_receipt mode

This commit is contained in:
Suherdy Yacob 2026-05-29 09:56:26 +07:00
parent d4416d9e66
commit 458404ba7a

View File

@ -52,9 +52,27 @@ const originalPrintHtml = PosPrinterService.prototype.printHtml;
patch(PosPrinterService.prototype, { patch(PosPrinterService.prototype, {
setup(env) { setup(env) {
this.env = env; this.env = env;
this._currentPrintOrder = null; // Store current order for data extraction
super.setup(...arguments); 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 * Override the printHtml method to use bluetooth printer
* Falls back to browser print on any failure * 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); console.log('[BluetoothPrint] Setting character set on generator:', escposGenerator.characterSet);
} }
// Parse receipt data from HTML element // Prefer direct model data over HTML parsing — HTML parsing fails in basic_receipt mode
console.log('[BluetoothPrint] Parsing receipt data from HTML...'); // because prices and totals are not rendered in the DOM when basic_receipt=true.
const receiptData = this._parseReceiptDataFromHtml(el); let receiptData;
console.log('[BluetoothPrint] Parsed receipt data:', JSON.stringify(receiptData, null, 2)); 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 // Generate ESC/POS commands
console.log('[BluetoothPrint] Generating ESC/POS text 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 * 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 * @private
* @param {HTMLElement} el - Receipt HTML element * @param {HTMLElement} el - Receipt HTML element
* @returns {Object} Structured receipt data * @returns {Object} Structured receipt data