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
|
||||
// Odoo 19: order reference is in .pos-receipt-vat, date in #order-date, cashier in .cashier
|
||||
const orderData = {
|
||||
orderName: getText('.pos-receipt-order-name') || getText('.order-name') || getText('.pos-receipt-order-data') || '',
|
||||
date: getText('.pos-receipt-date') || new Date().toLocaleString(),
|
||||
cashier: getText('.pos-receipt-cashier') || getText('.cashier') || '',
|
||||
orderName: getText('.pos-receipt-vat') || getText('.pos-receipt-order-name') || getText('.order-name') || getText('.pos-receipt-order-data') || '',
|
||||
date: getText('#order-date') || getText('.pos-receipt-date') || new Date().toLocaleString(),
|
||||
cashier: getText('.cashier') || getText('.pos-receipt-cashier') || '',
|
||||
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...');
|
||||
let lineElements = getAll('.orderline');
|
||||
let lineElements = getAll('li.orderline');
|
||||
if (lineElements.length === 0) {
|
||||
lineElements = getAll('.orderline');
|
||||
}
|
||||
if (lineElements.length === 0) {
|
||||
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) {
|
||||
// Try to find any table rows
|
||||
lineElements = getAll('tbody tr');
|
||||
@ -657,26 +656,75 @@ patch(PosPrinterService.prototype, {
|
||||
console.log('[BluetoothPrint] Found', lineElements.length, 'line elements');
|
||||
|
||||
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() || '';
|
||||
const qtyText = line.querySelector('.qty, .quantity')?.textContent.trim() || '1';
|
||||
const priceText = line.querySelector('.price, .price-unit')?.textContent.trim() || '0';
|
||||
const totalText = line.querySelector('.price-total, .total, td:last-child, .pos-receipt-right-align')?.textContent.trim() || '0';
|
||||
// Odoo 19 orderline structure:
|
||||
// div.product-name
|
||||
// span.qty ← quantity (e.g. "999")
|
||||
// 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)
|
||||
|
||||
console.log(`[BluetoothPrint] Line ${index} raw data:`, { productName, qtyText, priceText, totalText });
|
||||
// Get qty from the dedicated .qty span
|
||||
const qtySpan = line.querySelector('.qty');
|
||||
const qtyText = qtySpan ? qtySpan.textContent.trim() : '1';
|
||||
|
||||
// Parse numbers (remove currency symbols and commas)
|
||||
// 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 });
|
||||
|
||||
// Parse numbers (remove currency symbols and commas, keep digits/dot/minus)
|
||||
const parseNumber = (str) => {
|
||||
if (!str) return 0;
|
||||
const cleaned = str.replace(/[^0-9.-]/g, '');
|
||||
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 = {
|
||||
productName: productName,
|
||||
quantity: parseNumber(qtyText),
|
||||
price: parseNumber(priceText),
|
||||
total: parseNumber(totalText)
|
||||
quantity: qty,
|
||||
price: unitPrice,
|
||||
total: lineTotal
|
||||
};
|
||||
|
||||
console.log(`[BluetoothPrint] Line ${index} parsed:`, parsedLine);
|
||||
@ -688,12 +736,34 @@ patch(PosPrinterService.prototype, {
|
||||
console.log('[BluetoothPrint] All lines:', JSON.stringify(lines, null, 2));
|
||||
|
||||
// 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...');
|
||||
|
||||
// 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 = {
|
||||
subtotal: this._parseAmount(getText('.pos-receipt-subtotal, .subtotal')),
|
||||
tax: this._parseAmount(getText('.pos-receipt-tax, .tax')),
|
||||
discount: this._parseAmount(getText('.pos-receipt-discount, .discount')),
|
||||
total: this._parseAmount(getText('.pos-receipt-total, .total, .amount-total'))
|
||||
subtotal: subtotalText ? this._parseAmount(subtotalText) : 0,
|
||||
tax: taxTotal,
|
||||
discount: this._parseAmount(getText('.label-discount ~ .pos-receipt-right-align')),
|
||||
total: this._parseAmount(totalText)
|
||||
};
|
||||
|
||||
console.log('[BluetoothPrint] Parsed totals:', totals);
|
||||
@ -707,15 +777,37 @@ patch(PosPrinterService.prototype, {
|
||||
}
|
||||
|
||||
// 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 = {
|
||||
method: getText('.pos-receipt-payment-method, .payment-method') || 'Cash',
|
||||
amount: this._parseAmount(getText('.pos-receipt-payment-amount, .payment-amount')) || totals.total,
|
||||
change: this._parseAmount(getText('.pos-receipt-change, .change')) || 0
|
||||
method: paymentMethod,
|
||||
amount: paymentAmount,
|
||||
change: changeAmount
|
||||
};
|
||||
|
||||
// Footer
|
||||
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
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user