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 };