Fix QZ multi-printer fallback and kitchen hook

This commit is contained in:
metatroncubeswdev 2026-05-07 11:25:37 -04:00
parent e7c67653e4
commit c94c1f06eb
4 changed files with 65 additions and 47 deletions

View File

@ -1,6 +1,6 @@
{ {
'name': 'Dine360 QZ Tray Printer', 'name': 'Dine360 QZ Tray Printer',
'version': '17.0.1.2', 'version': '17.0.1.3',
'category': 'Point of Sale', 'category': 'Point of Sale',
'summary': 'Integrate Odoo POS with Star/Epson Printers via QZ Tray.', 'summary': 'Integrate Odoo POS with Star/Epson Printers via QZ Tray.',
'depends': ['point_of_sale'], 'depends': ['point_of_sale'],

View File

@ -4,6 +4,7 @@ class PosConfig(models.Model):
_inherit = 'pos.config' _inherit = 'pos.config'
use_qz_printer = fields.Boolean("Use QZ Tray Printer", help="Print directly using QZ Tray locally") 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_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_kitchen_printer_name = fields.Char("Kitchen Printer Name", help="Name of the kitchen printer mapped in QZ Tray")
qz_paper_width = fields.Selection( qz_paper_width = fields.Selection(

View File

@ -5,6 +5,7 @@ class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings' _inherit = 'res.config.settings'
use_qz_printer = fields.Boolean(related='pos_config_id.use_qz_printer', readonly=False) 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_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_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) qz_paper_width = fields.Selection(related='pos_config_id.qz_paper_width', readonly=False)

View File

@ -3,7 +3,7 @@
import { patch } from "@web/core/utils/patch"; import { patch } from "@web/core/utils/patch";
import { ReceiptScreen } from "@point_of_sale/app/screens/receipt_screen/receipt_screen"; import { ReceiptScreen } from "@point_of_sale/app/screens/receipt_screen/receipt_screen";
import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup"; 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 ESC = 0x1b;
const GS = 0x1d; const GS = 0x1d;
@ -17,6 +17,10 @@ function columnsFromConfig(config) {
return Number.isFinite(value) ? value : DEFAULT_COLUMNS; return Number.isFinite(value) ? value : DEFAULT_COLUMNS;
} }
function getBillingPrinterName(config) {
return config?.qz_billing_printer_name || config?.qz_printer_name || "";
}
function cleanText(value) { function cleanText(value) {
return String(value ?? "") return String(value ?? "")
.normalize("NFKD") .normalize("NFKD")
@ -356,7 +360,8 @@ async function buildEscPosReceipt(order, pos) {
patch(ReceiptScreen.prototype, { patch(ReceiptScreen.prototype, {
async printReceipt() { 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 { try {
if (!window.qz) { if (!window.qz) {
console.error("QZ Tray library not loaded."); console.error("QZ Tray library not loaded.");
@ -367,7 +372,7 @@ patch(ReceiptScreen.prototype, {
await qz.websocket.connect({ retries: 2, delay: 1 }); 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", encoding: "CP437",
copies: 1, copies: 1,
spool: { size: 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 config = pos?.config || {};
const columns = columnsFromConfig(config); const columns = columnsFromConfig(config);
const builder = new EscPosBuilder(columns); const builder = new EscPosBuilder(columns);
const table = order.getTable?.() || order.table;
const newLines = orderChange?.new || [];
const cancelledLines = orderChange?.cancelled || [];
builder.init(); builder.init();
builder.align("center"); builder.align("center");
@ -412,19 +420,25 @@ async function buildEscPosKitchenTicket(order, pos, changes) {
builder.align("left"); builder.align("left");
if (order.name) builder.line(`Order: ${order.name}`); 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}`); if (order.customer_count) builder.line(`Guests: ${order.customer_count}`);
builder.line(`Time: ${new Date().toLocaleTimeString()}`); builder.line(`Time: ${new Date().toLocaleTimeString()}`);
if (cancelled) {
builder.bold(true);
builder.line("ORDER CANCELLED");
builder.bold(false);
}
builder.separator("="); builder.separator("=");
for (const category of Object.values(changes.categories || {})) { function printSection(title, lines) {
if (category.name) { if (!lines.length) {
builder.bold(true); return;
builder.line(category.name.toUpperCase());
builder.bold(false);
} }
for (const line of category.orderlines || []) { builder.bold(true);
const qty = line.quantity; 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+$/, ""); const qtyText = Number.isInteger(qty) ? String(qty) : qty.toFixed(2).replace(/\.?0+$/, "");
addWrappedLine(builder, `${qtyText} x ${line.name}`); addWrappedLine(builder, `${qtyText} x ${line.name}`);
if (line.note) { if (line.note) {
@ -434,6 +448,9 @@ async function buildEscPosKitchenTicket(order, pos, changes) {
builder.line(""); builder.line("");
} }
printSection("NEW ITEMS", newLines);
printSection("CANCELLED ITEMS", cancelledLines);
builder.feed(END_FEED_LINES); builder.feed(END_FEED_LINES);
if (config.qz_enable_cutter !== false) { if (config.qz_enable_cutter !== false) {
builder.cut(config.qz_cut_mode || "partial"); builder.cut(config.qz_cut_mode || "partial");
@ -442,41 +459,40 @@ async function buildEscPosKitchenTicket(order, pos, changes) {
return bytesToBase64(builder.bytes); return bytesToBase64(builder.bytes);
} }
patch(PosStore.prototype, { patch(Order.prototype, {
async sendOrderToPrinter(order) { async printChanges(cancelled) {
if (this.config.use_qz_printer && this.config.qz_kitchen_printer_name) { const printerName = this.pos.config.qz_kitchen_printer_name;
const changes = order.computeChanges(); if (this.pos.config.use_qz_printer && printerName) {
if (Object.keys(changes.categories).length > 0) { const orderChange = this.changesToOrder(cancelled);
try { if (!orderChange.new.length && !orderChange.cancelled.length) {
if (!window.qz) { return true;
console.error("QZ Tray library not loaded."); }
return false; try {
} if (!window.qz) {
if (!qz.websocket.isActive()) { console.error("QZ Tray library not loaded.");
await qz.websocket.connect({ retries: 2, delay: 1 }); return false;
} }
if (!qz.websocket.isActive()) {
const config = qz.configs.create(this.config.qz_kitchen_printer_name, { await qz.websocket.connect({ retries: 2, delay: 1 });
encoding: "CP437", }
copies: 1, const config = qz.configs.create(printerName, {
spool: { size: 1 }, encoding: "CP437",
}); copies: 1,
const payload = await buildEscPosKitchenTicket(order, this, changes); spool: { size: 1 },
await qz.print(config, [{ });
type: "raw", const payload = await buildEscPosKitchenTicket(this, this.pos, orderChange, cancelled);
format: "command", await qz.print(config, [{
flavor: "base64", type: "raw",
data: payload, format: "command",
}]); flavor: "base64",
order.saved_quantity = order.get_orderlines().reduce((acc, line) => acc + line.get_quantity(), 0); data: payload,
return true; }]);
} catch (err) { return true;
console.error("QZ Tray Kitchen Print Error:", err); } catch (err) {
// Fallback or show error console.error("QZ Tray Kitchen Print Error:", err);
} return false;
} }
return true;
} }
return super.sendOrderToPrinter(...arguments); return super.printChanges(...arguments);
}, },
}); });