diff --git a/static/src/js/pos_receipt_printer.js b/static/src/js/pos_receipt_printer.js
index 1988a1b..b9d06de 100755
--- a/static/src/js/pos_receipt_printer.js
+++ b/static/src/js/pos_receipt_printer.js
@@ -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
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)
+
+ // 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)
+ // 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
};