feat: implement direct POS order model data extraction to ensure accurate receipt printing in basic_receipt mode
This commit is contained in:
parent
d4416d9e66
commit
458404ba7a
@ -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,8 +619,118 @@ 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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user