diff --git a/addons/dine360_qz_printer/__manifest__.py b/addons/dine360_qz_printer/__manifest__.py index 11bfe6b..a5e1ffd 100644 --- a/addons/dine360_qz_printer/__manifest__.py +++ b/addons/dine360_qz_printer/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Dine360 QZ Tray Printer', - 'version': '17.0.1.2', + 'version': '17.0.1.3', 'category': 'Point of Sale', 'summary': 'Integrate Odoo POS with Star/Epson Printers via QZ Tray.', 'depends': ['point_of_sale'], diff --git a/addons/dine360_qz_printer/models/pos_config.py b/addons/dine360_qz_printer/models/pos_config.py index 56d40d4..8a4fead 100644 --- a/addons/dine360_qz_printer/models/pos_config.py +++ b/addons/dine360_qz_printer/models/pos_config.py @@ -4,6 +4,7 @@ class PosConfig(models.Model): _inherit = 'pos.config' use_qz_printer = fields.Boolean("Use QZ Tray Printer", help="Print directly using QZ Tray locally") + qz_printer_name = fields.Char("Legacy QZ Printer Name", help="Previous single-printer setting kept for compatibility") qz_billing_printer_name = fields.Char("Billing Printer Name", help="Name of the billing printer mapped in QZ Tray") qz_kitchen_printer_name = fields.Char("Kitchen Printer Name", help="Name of the kitchen printer mapped in QZ Tray") qz_paper_width = fields.Selection( diff --git a/addons/dine360_qz_printer/models/res_config_settings.py b/addons/dine360_qz_printer/models/res_config_settings.py index 48a1d53..1e86d50 100644 --- a/addons/dine360_qz_printer/models/res_config_settings.py +++ b/addons/dine360_qz_printer/models/res_config_settings.py @@ -5,6 +5,7 @@ class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' use_qz_printer = fields.Boolean(related='pos_config_id.use_qz_printer', readonly=False) + qz_printer_name = fields.Char(related='pos_config_id.qz_printer_name', readonly=False) qz_billing_printer_name = fields.Char(related='pos_config_id.qz_billing_printer_name', readonly=False) qz_kitchen_printer_name = fields.Char(related='pos_config_id.qz_kitchen_printer_name', readonly=False) qz_paper_width = fields.Selection(related='pos_config_id.qz_paper_width', readonly=False) diff --git a/addons/dine360_qz_printer/static/src/js/qz_wrapper.js b/addons/dine360_qz_printer/static/src/js/qz_wrapper.js index 566e6ee..cfbfe73 100644 --- a/addons/dine360_qz_printer/static/src/js/qz_wrapper.js +++ b/addons/dine360_qz_printer/static/src/js/qz_wrapper.js @@ -3,7 +3,7 @@ import { patch } from "@web/core/utils/patch"; import { ReceiptScreen } from "@point_of_sale/app/screens/receipt_screen/receipt_screen"; import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup"; -import { PosStore } from "@point_of_sale/app/store/pos_store"; +import { Order } from "@point_of_sale/app/store/models"; const ESC = 0x1b; const GS = 0x1d; @@ -17,6 +17,10 @@ function columnsFromConfig(config) { return Number.isFinite(value) ? value : DEFAULT_COLUMNS; } +function getBillingPrinterName(config) { + return config?.qz_billing_printer_name || config?.qz_printer_name || ""; +} + function cleanText(value) { return String(value ?? "") .normalize("NFKD") @@ -356,7 +360,8 @@ async function buildEscPosReceipt(order, pos) { patch(ReceiptScreen.prototype, { async printReceipt() { - if (this.pos.config.use_qz_printer && this.pos.config.qz_billing_printer_name) { + const printerName = getBillingPrinterName(this.pos.config); + if (this.pos.config.use_qz_printer && printerName) { try { if (!window.qz) { console.error("QZ Tray library not loaded."); @@ -367,7 +372,7 @@ patch(ReceiptScreen.prototype, { await qz.websocket.connect({ retries: 2, delay: 1 }); } - const config = qz.configs.create(this.pos.config.qz_billing_printer_name, { + const config = qz.configs.create(printerName, { encoding: "CP437", copies: 1, spool: { size: 1 }, @@ -397,10 +402,13 @@ patch(ReceiptScreen.prototype, { }, }); -async function buildEscPosKitchenTicket(order, pos, changes) { +async function buildEscPosKitchenTicket(order, pos, orderChange, cancelled = false) { const config = pos?.config || {}; const columns = columnsFromConfig(config); const builder = new EscPosBuilder(columns); + const table = order.getTable?.() || order.table; + const newLines = orderChange?.new || []; + const cancelledLines = orderChange?.cancelled || []; builder.init(); builder.align("center"); @@ -412,19 +420,25 @@ async function buildEscPosKitchenTicket(order, pos, changes) { builder.align("left"); if (order.name) builder.line(`Order: ${order.name}`); - if (order.table?.name) builder.line(`Table: ${order.table.name}`); + if (table?.name) builder.line(`Table: ${table.name}`); if (order.customer_count) builder.line(`Guests: ${order.customer_count}`); builder.line(`Time: ${new Date().toLocaleTimeString()}`); + if (cancelled) { + builder.bold(true); + builder.line("ORDER CANCELLED"); + builder.bold(false); + } builder.separator("="); - for (const category of Object.values(changes.categories || {})) { - if (category.name) { - builder.bold(true); - builder.line(category.name.toUpperCase()); - builder.bold(false); + function printSection(title, lines) { + if (!lines.length) { + return; } - for (const line of category.orderlines || []) { - const qty = line.quantity; + builder.bold(true); + builder.line(title); + builder.bold(false); + for (const line of lines) { + const qty = Number(line.quantity || 0); const qtyText = Number.isInteger(qty) ? String(qty) : qty.toFixed(2).replace(/\.?0+$/, ""); addWrappedLine(builder, `${qtyText} x ${line.name}`); if (line.note) { @@ -434,6 +448,9 @@ async function buildEscPosKitchenTicket(order, pos, changes) { builder.line(""); } + printSection("NEW ITEMS", newLines); + printSection("CANCELLED ITEMS", cancelledLines); + builder.feed(END_FEED_LINES); if (config.qz_enable_cutter !== false) { builder.cut(config.qz_cut_mode || "partial"); @@ -442,41 +459,40 @@ async function buildEscPosKitchenTicket(order, pos, changes) { return bytesToBase64(builder.bytes); } -patch(PosStore.prototype, { - async sendOrderToPrinter(order) { - if (this.config.use_qz_printer && this.config.qz_kitchen_printer_name) { - const changes = order.computeChanges(); - if (Object.keys(changes.categories).length > 0) { - try { - if (!window.qz) { - console.error("QZ Tray library not loaded."); - return false; - } - if (!qz.websocket.isActive()) { - await qz.websocket.connect({ retries: 2, delay: 1 }); - } - - const config = qz.configs.create(this.config.qz_kitchen_printer_name, { - encoding: "CP437", - copies: 1, - spool: { size: 1 }, - }); - const payload = await buildEscPosKitchenTicket(order, this, changes); - await qz.print(config, [{ - type: "raw", - format: "command", - flavor: "base64", - data: payload, - }]); - order.saved_quantity = order.get_orderlines().reduce((acc, line) => acc + line.get_quantity(), 0); - return true; - } catch (err) { - console.error("QZ Tray Kitchen Print Error:", err); - // Fallback or show error - } +patch(Order.prototype, { + async printChanges(cancelled) { + const printerName = this.pos.config.qz_kitchen_printer_name; + if (this.pos.config.use_qz_printer && printerName) { + const orderChange = this.changesToOrder(cancelled); + if (!orderChange.new.length && !orderChange.cancelled.length) { + return true; + } + try { + if (!window.qz) { + console.error("QZ Tray library not loaded."); + return false; + } + if (!qz.websocket.isActive()) { + await qz.websocket.connect({ retries: 2, delay: 1 }); + } + const config = qz.configs.create(printerName, { + encoding: "CP437", + copies: 1, + spool: { size: 1 }, + }); + const payload = await buildEscPosKitchenTicket(this, this.pos, orderChange, cancelled); + await qz.print(config, [{ + type: "raw", + format: "command", + flavor: "base64", + data: payload, + }]); + return true; + } catch (err) { + console.error("QZ Tray Kitchen Print Error:", err); + return false; } - return true; } - return super.sendOrderToPrinter(...arguments); + return super.printChanges(...arguments); }, });