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, {
|
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,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
|
* 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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user