feat: update pos_receipt_printer to support Odoo 19 receipt structure and element selectors
This commit is contained in:
parent
4ee383c672
commit
d4416d9e66
@ -630,25 +630,24 @@ patch(PosPrinterService.prototype, {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Parse order info
|
// Parse order info
|
||||||
|
// Odoo 19: order reference is in .pos-receipt-vat, date in #order-date, cashier in .cashier
|
||||||
const orderData = {
|
const orderData = {
|
||||||
orderName: getText('.pos-receipt-order-name') || getText('.order-name') || getText('.pos-receipt-order-data') || '',
|
orderName: getText('.pos-receipt-vat') || getText('.pos-receipt-order-name') || getText('.order-name') || getText('.pos-receipt-order-data') || '',
|
||||||
date: getText('.pos-receipt-date') || new Date().toLocaleString(),
|
date: getText('#order-date') || getText('.pos-receipt-date') || new Date().toLocaleString(),
|
||||||
cashier: getText('.pos-receipt-cashier') || getText('.cashier') || '',
|
cashier: getText('.cashier') || getText('.pos-receipt-cashier') || '',
|
||||||
customer: getText('.pos-receipt-customer') || getText('.customer') || null
|
customer: getText('.pos-receipt-customer') || getText('.customer') || null
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse order lines - try multiple selectors
|
// Parse order lines
|
||||||
|
// Odoo 19: each order line is an <li class="orderline"> element
|
||||||
console.log('[BluetoothPrint] Searching for order lines...');
|
console.log('[BluetoothPrint] Searching for order lines...');
|
||||||
let lineElements = getAll('.orderline');
|
let lineElements = getAll('li.orderline');
|
||||||
|
if (lineElements.length === 0) {
|
||||||
|
lineElements = getAll('.orderline');
|
||||||
|
}
|
||||||
if (lineElements.length === 0) {
|
if (lineElements.length === 0) {
|
||||||
lineElements = getAll('.pos-receipt-orderline');
|
lineElements = getAll('.pos-receipt-orderline');
|
||||||
}
|
}
|
||||||
if (lineElements.length === 0) {
|
|
||||||
lineElements = getAll('tr.orderline');
|
|
||||||
}
|
|
||||||
if (lineElements.length === 0) {
|
|
||||||
lineElements = getAll('.pos-orderline');
|
|
||||||
}
|
|
||||||
if (lineElements.length === 0) {
|
if (lineElements.length === 0) {
|
||||||
// Try to find any table rows
|
// Try to find any table rows
|
||||||
lineElements = getAll('tbody tr');
|
lineElements = getAll('tbody tr');
|
||||||
@ -657,26 +656,75 @@ patch(PosPrinterService.prototype, {
|
|||||||
console.log('[BluetoothPrint] Found', lineElements.length, 'line elements');
|
console.log('[BluetoothPrint] Found', lineElements.length, 'line elements');
|
||||||
|
|
||||||
const lines = lineElements.map((line, index) => {
|
const lines = lineElements.map((line, index) => {
|
||||||
console.log(`[BluetoothPrint] Processing line ${index}:`, line.outerHTML.substring(0, 200));
|
console.log(`[BluetoothPrint] Processing line ${index}:`, line.outerHTML.substring(0, 300));
|
||||||
|
|
||||||
const productName = line.querySelector('.product-name, td:first-child, .pos-receipt-left-align')?.textContent.trim() || '';
|
// Odoo 19 orderline structure:
|
||||||
const qtyText = line.querySelector('.qty, .quantity')?.textContent.trim() || '1';
|
// div.product-name
|
||||||
const priceText = line.querySelector('.price, .price-unit')?.textContent.trim() || '0';
|
// span.qty ← quantity (e.g. "999")
|
||||||
const totalText = line.querySelector('.price-total, .total, td:last-child, .pos-receipt-right-align')?.textContent.trim() || '0';
|
// span.text-wrap ← product name only
|
||||||
|
// div.product-price.price ← line total (formatted currency, e.g. "Rp 0.00")
|
||||||
|
// ul.info-list
|
||||||
|
// li.price-per-unit ← "unit_price / UoM" (only shown when qty != 1)
|
||||||
|
|
||||||
|
// Get qty from the dedicated .qty span
|
||||||
|
const qtySpan = line.querySelector('.qty');
|
||||||
|
const qtyText = qtySpan ? qtySpan.textContent.trim() : '1';
|
||||||
|
|
||||||
|
// Get product name from span.text-wrap (excludes the qty span)
|
||||||
|
const nameSpan = line.querySelector('.product-name .text-wrap');
|
||||||
|
let productName = '';
|
||||||
|
if (nameSpan) {
|
||||||
|
productName = nameSpan.textContent.trim();
|
||||||
|
} else {
|
||||||
|
// Fallback: get .product-name text then strip the qty prefix
|
||||||
|
const productNameEl = line.querySelector('.product-name');
|
||||||
|
if (productNameEl) {
|
||||||
|
const fullText = productNameEl.textContent.trim();
|
||||||
|
const qtyPrefix = qtyText;
|
||||||
|
productName = fullText.startsWith(qtyPrefix)
|
||||||
|
? fullText.slice(qtyPrefix.length).trim()
|
||||||
|
: fullText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line total: div.product-price.price contains the formatted currency string
|
||||||
|
const priceTotalEl = line.querySelector('.product-price.price, .product-price');
|
||||||
|
const priceTotalText = priceTotalEl ? priceTotalEl.textContent.trim() : '0';
|
||||||
|
|
||||||
|
// Unit price: li.price-per-unit contains "unit_price / UoM"
|
||||||
|
// Only shown when qty != 1; format: "Rp 1,234.56 / Unit"
|
||||||
|
const priceUnitEl = line.querySelector('.price-per-unit');
|
||||||
|
const priceUnitText = priceUnitEl ? priceUnitEl.textContent.trim() : '';
|
||||||
|
|
||||||
|
console.log(`[BluetoothPrint] Line ${index} raw data:`, { productName, qtyText, priceTotalText, priceUnitText });
|
||||||
|
|
||||||
console.log(`[BluetoothPrint] Line ${index} raw data:`, { productName, qtyText, priceText, totalText });
|
// Parse numbers (remove currency symbols and commas, keep digits/dot/minus)
|
||||||
|
|
||||||
// Parse numbers (remove currency symbols and commas)
|
|
||||||
const parseNumber = (str) => {
|
const parseNumber = (str) => {
|
||||||
|
if (!str) return 0;
|
||||||
const cleaned = str.replace(/[^0-9.-]/g, '');
|
const cleaned = str.replace(/[^0-9.-]/g, '');
|
||||||
return parseFloat(cleaned) || 0;
|
return parseFloat(cleaned) || 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const qty = parseNumber(qtyText);
|
||||||
|
const lineTotal = parseNumber(priceTotalText);
|
||||||
|
|
||||||
|
// Extract unit price from "price / UoM" string (take part before " / ")
|
||||||
|
let unitPrice = 0;
|
||||||
|
if (priceUnitText) {
|
||||||
|
const slashIdx = priceUnitText.lastIndexOf('/');
|
||||||
|
const priceStr = slashIdx > 0 ? priceUnitText.substring(0, slashIdx) : priceUnitText;
|
||||||
|
unitPrice = parseNumber(priceStr);
|
||||||
|
} else if (qty > 0 && lineTotal > 0) {
|
||||||
|
unitPrice = lineTotal / qty;
|
||||||
|
} else {
|
||||||
|
unitPrice = lineTotal;
|
||||||
|
}
|
||||||
|
|
||||||
const parsedLine = {
|
const parsedLine = {
|
||||||
productName: productName,
|
productName: productName,
|
||||||
quantity: parseNumber(qtyText),
|
quantity: qty,
|
||||||
price: parseNumber(priceText),
|
price: unitPrice,
|
||||||
total: parseNumber(totalText)
|
total: lineTotal
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`[BluetoothPrint] Line ${index} parsed:`, parsedLine);
|
console.log(`[BluetoothPrint] Line ${index} parsed:`, parsedLine);
|
||||||
@ -688,12 +736,34 @@ patch(PosPrinterService.prototype, {
|
|||||||
console.log('[BluetoothPrint] All lines:', JSON.stringify(lines, null, 2));
|
console.log('[BluetoothPrint] All lines:', JSON.stringify(lines, null, 2));
|
||||||
|
|
||||||
// Parse totals
|
// Parse totals
|
||||||
|
// Odoo 19 receipt structure:
|
||||||
|
// div.pos-receipt-amount.receipt-total > span.label-total + span.pos-receipt-right-align (total)
|
||||||
|
// div.pos-receipt-taxes > div > span (subtotal excl. tax) + div > span (per tax group amount)
|
||||||
|
// div.pos-receipt-amount.receipt-change > span.pos-receipt-right-align (change)
|
||||||
console.log('[BluetoothPrint] Parsing totals...');
|
console.log('[BluetoothPrint] Parsing totals...');
|
||||||
|
|
||||||
|
// Grand total
|
||||||
|
const totalEl = el.querySelector('.receipt-total .pos-receipt-right-align, .receipt-total .font-monospace');
|
||||||
|
const totalText = totalEl ? totalEl.textContent.trim() : '';
|
||||||
|
|
||||||
|
// Subtotal (excl. tax) — only present when tax groups are shown
|
||||||
|
const subtotalEl = el.querySelector('.pos-receipt-taxes .ms-auto');
|
||||||
|
const subtotalText = subtotalEl ? subtotalEl.textContent.trim() : '';
|
||||||
|
|
||||||
|
// Tax amounts (all .font-monospace inside .pos-receipt-taxes except the subtotal span)
|
||||||
|
let taxTotal = 0;
|
||||||
|
const taxGroupDivs = el.querySelectorAll('.pos-receipt-taxes > div');
|
||||||
|
taxGroupDivs.forEach((div, i) => {
|
||||||
|
if (i === 0) return; // First div is the subtotal row
|
||||||
|
const amtEl = div.querySelector('.font-monospace, .ms-auto');
|
||||||
|
if (amtEl) taxTotal += this._parseAmount(amtEl.textContent.trim());
|
||||||
|
});
|
||||||
|
|
||||||
const totals = {
|
const totals = {
|
||||||
subtotal: this._parseAmount(getText('.pos-receipt-subtotal, .subtotal')),
|
subtotal: subtotalText ? this._parseAmount(subtotalText) : 0,
|
||||||
tax: this._parseAmount(getText('.pos-receipt-tax, .tax')),
|
tax: taxTotal,
|
||||||
discount: this._parseAmount(getText('.pos-receipt-discount, .discount')),
|
discount: this._parseAmount(getText('.label-discount ~ .pos-receipt-right-align')),
|
||||||
total: this._parseAmount(getText('.pos-receipt-total, .total, .amount-total'))
|
total: this._parseAmount(totalText)
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[BluetoothPrint] Parsed totals:', totals);
|
console.log('[BluetoothPrint] Parsed totals:', totals);
|
||||||
@ -707,15 +777,37 @@ patch(PosPrinterService.prototype, {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse payment info
|
// Parse payment info
|
||||||
|
// Odoo 19: div.paymentlines contains payment method name (text node) + span amount
|
||||||
|
let paymentMethod = 'Cash';
|
||||||
|
let paymentAmount = totals.total;
|
||||||
|
|
||||||
|
const paymentLineEls = el.querySelectorAll('.paymentlines');
|
||||||
|
if (paymentLineEls.length > 0) {
|
||||||
|
const firstPayLine = paymentLineEls[0];
|
||||||
|
const amtSpan = firstPayLine.querySelector('.pos-receipt-right-align, .font-monospace');
|
||||||
|
if (amtSpan) {
|
||||||
|
paymentAmount = this._parseAmount(amtSpan.textContent.trim()) || totals.total;
|
||||||
|
// Method name is everything in the div except the amount span
|
||||||
|
const methodText = firstPayLine.textContent.replace(amtSpan.textContent, '').trim();
|
||||||
|
paymentMethod = methodText || 'Cash';
|
||||||
|
} else {
|
||||||
|
paymentMethod = firstPayLine.textContent.trim() || 'Cash';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change
|
||||||
|
const changeEl = el.querySelector('.receipt-change .pos-receipt-right-align, .receipt-change .font-monospace');
|
||||||
|
const changeAmount = changeEl ? this._parseAmount(changeEl.textContent.trim()) : 0;
|
||||||
|
|
||||||
const paymentData = {
|
const paymentData = {
|
||||||
method: getText('.pos-receipt-payment-method, .payment-method') || 'Cash',
|
method: paymentMethod,
|
||||||
amount: this._parseAmount(getText('.pos-receipt-payment-amount, .payment-amount')) || totals.total,
|
amount: paymentAmount,
|
||||||
change: this._parseAmount(getText('.pos-receipt-change, .change')) || 0
|
change: changeAmount
|
||||||
};
|
};
|
||||||
|
|
||||||
// Footer
|
// Footer
|
||||||
const footerData = {
|
const footerData = {
|
||||||
message: getText('.pos-receipt-footer, .receipt-footer') || 'Thank you for your business!',
|
message: getText('.pos-receipt-center-align, .pos-receipt-footer, .receipt-footer') || 'Thank you for your business!',
|
||||||
barcode: orderData.orderName || null
|
barcode: orderData.orderName || null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user