From 20ff5dc1a4da99718f1b5539481c84346751d41d Mon Sep 17 00:00:00 2001 From: Miquel Alzanillas Date: Tue, 28 Apr 2026 15:16:06 +0200 Subject: [PATCH 1/4] [ADD] pos_printing_qztray: New Module --- pos_printing_qztray/README.rst | 164 ++++++ pos_printing_qztray/__init__.py | 4 + pos_printing_qztray/__manifest__.py | 31 ++ pos_printing_qztray/controllers/__init__.py | 3 + pos_printing_qztray/controllers/main.py | 263 +++++++++ pos_printing_qztray/i18n/es.po | 150 ++++++ pos_printing_qztray/i18n/it.po | 142 +++++ .../i18n/pos_printing_qztray.pot | 141 +++++ pos_printing_qztray/models/__init__.py | 3 + pos_printing_qztray/models/pos_config.py | 18 + pos_printing_qztray/pyproject.toml | 3 + pos_printing_qztray/readme/CONFIGURE.md | 15 + pos_printing_qztray/readme/CONTRIBUTORS.md | 1 + pos_printing_qztray/readme/DESCRIPTION.md | 15 + pos_printing_qztray/readme/INSTALL.md | 13 + pos_printing_qztray/readme/ROADMAP.md | 0 pos_printing_qztray/readme/USAGE.md | 12 + .../readme/newsfragments/.gitkeep | 0 .../static/description/icon.png | Bin 0 -> 10254 bytes .../static/description/icon.svg | 1 + .../static/description/index.html | 506 ++++++++++++++++++ .../src/app/printer/qz_tray_connection.esm.js | 74 +++ .../src/app/printer/qz_tray_printer.esm.js | 94 ++++ .../services/qz_tray_printer_service.esm.js | 45 ++ pos_printing_qztray/tests/__init__.py | 2 + .../tests/test_escpos_controller.py | 55 ++ pos_printing_qztray/tests/test_pos_config.py | 38 ++ .../views/pos_config_views.xml | 45 ++ 28 files changed, 1838 insertions(+) create mode 100644 pos_printing_qztray/README.rst create mode 100644 pos_printing_qztray/__init__.py create mode 100644 pos_printing_qztray/__manifest__.py create mode 100644 pos_printing_qztray/controllers/__init__.py create mode 100644 pos_printing_qztray/controllers/main.py create mode 100644 pos_printing_qztray/i18n/es.po create mode 100644 pos_printing_qztray/i18n/it.po create mode 100644 pos_printing_qztray/i18n/pos_printing_qztray.pot create mode 100644 pos_printing_qztray/models/__init__.py create mode 100644 pos_printing_qztray/models/pos_config.py create mode 100644 pos_printing_qztray/pyproject.toml create mode 100644 pos_printing_qztray/readme/CONFIGURE.md create mode 100644 pos_printing_qztray/readme/CONTRIBUTORS.md create mode 100644 pos_printing_qztray/readme/DESCRIPTION.md create mode 100644 pos_printing_qztray/readme/INSTALL.md create mode 100644 pos_printing_qztray/readme/ROADMAP.md create mode 100644 pos_printing_qztray/readme/USAGE.md create mode 100644 pos_printing_qztray/readme/newsfragments/.gitkeep create mode 100644 pos_printing_qztray/static/description/icon.png create mode 100644 pos_printing_qztray/static/description/icon.svg create mode 100644 pos_printing_qztray/static/description/index.html create mode 100644 pos_printing_qztray/static/src/app/printer/qz_tray_connection.esm.js create mode 100644 pos_printing_qztray/static/src/app/printer/qz_tray_printer.esm.js create mode 100644 pos_printing_qztray/static/src/app/services/qz_tray_printer_service.esm.js create mode 100644 pos_printing_qztray/tests/__init__.py create mode 100644 pos_printing_qztray/tests/test_escpos_controller.py create mode 100644 pos_printing_qztray/tests/test_pos_config.py create mode 100644 pos_printing_qztray/views/pos_config_views.xml diff --git a/pos_printing_qztray/README.rst b/pos_printing_qztray/README.rst new file mode 100644 index 0000000000..08312dc3ac --- /dev/null +++ b/pos_printing_qztray/README.rst @@ -0,0 +1,164 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +=============== +POS Printing QZ +=============== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:7a277f359b6482de0ea78cdef95a03e8baeee84f3b3eb1c3549005b8d5882529 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpos-lightgray.png?logo=github + :target: https://github.com/OCA/pos/tree/19.0/pos_printing_qztray + :alt: OCA/pos +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/pos-19-0/pos-19-0-pos_printing_qztray + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/pos&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module provides a pure-software alternative to the Odoo IoT Box for +thermal receipt printing in the Point of Sale, using [QZ +Tray](https://qz.io/) as the print driver. + +It extends base_report_to_printer_qz — which adds QZ Tray backend +support to printing.printer — and brings that functionality into the +POS, allowing users to select a printer with backend = qztray directly +from the POS configuration. + +Key features: + +- **Cash drawer support**: the cash drawer open command (ESC p) is sent + automatically after each receipt print. +- **No IoT Box required**: designed as a software-only alternative for + setups where deploying IoT infrastructure is not desirable. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +This module requires the following non-Python dependency: + +- [QZ Tray](https://qz.io/) must be installed and running on the client + machine that operates the POS. + +Odoo module dependency (must be installed first): + +- base_report_to_printer_qz — provides the QZ Tray printer backend and + the printing.printer model with backend = qztray support. + +Python dependencies (installed automatically via external_dependencies): + +- python-escpos + +Configuration +============= + +To configure this module, you need to: + +1. Install and launch the [QZ Tray](https://qz.io/) desktop application + on the machine where the thermal printer is connected. +2. In Odoo, go to *Point of Sale* > *Configuration* > *Settings*. +3. Select your POS configuration and enable **QZ Tray Printing**. +4. Optionally select a **QZ Tray Printer** from the list — only printers + with backend = qztray (managed by base_report_to_printer_qz) are + shown. If left empty, QZ Tray will use the system default printer. +5. Save the settings and open the POS session. + +To extend or customise the receipt template, inherit PosEscposController +in your own module and override the relevant \_get\_\* methods (e.g. +\_get_header, \_get_order_lines). No frontend changes are required. + +Usage +===== + +To use this module, you need to: + +1. Open a POS session that has **QZ Tray Printing** enabled. +2. Make sure the QZ Tray desktop application is running on the POS + machine. +3. Process a sale and click **Payment** to print the receipt. + +The receipt is sent directly to the thermal printer via QZ Tray. Receipt +HTML is rendered to PNG and converted to ESC/POS format server-side +before sending following native Odoo logic. The cash drawer is opened +automatically after each print if enabled. + +No IoT Box or network printer configuration is required. + +Known issues / Roadmap +====================== + + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* APSL Nagarro + +Contributors +------------ + +- Miquel Alzanillas + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-miquelalzanillas| image:: https://github.com/miquelalzanillas.png?size=40px + :target: https://github.com/miquelalzanillas + :alt: miquelalzanillas + +Current `maintainer `__: + +|maintainer-miquelalzanillas| + +This module is part of the `OCA/pos `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/pos_printing_qztray/__init__.py b/pos_printing_qztray/__init__.py new file mode 100644 index 0000000000..f4a659efb5 --- /dev/null +++ b/pos_printing_qztray/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models +from . import controllers diff --git a/pos_printing_qztray/__manifest__.py b/pos_printing_qztray/__manifest__.py new file mode 100644 index 0000000000..86ef26c47f --- /dev/null +++ b/pos_printing_qztray/__manifest__.py @@ -0,0 +1,31 @@ +# Copyright 2026 Miquel Alzanillas +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "POS Printing QZ", + "summary": "POS receipt printing via QZ Tray", + "version": "19.0.1.0.0", + # see https://odoo-community.org/page/development-status + "development_status": "Alpha", + "category": "Point Of Sale", + "website": "https://github.com/OCA/pos", + "author": "APSL Nagarro, Odoo Community Association (OCA)", + "maintainers": ["miquelalzanillas"], + "license": "AGPL-3", + "application": False, + "installable": True, + "external_dependencies": {"python": ["python-escpos"]}, + "depends": [ + "point_of_sale", + "base_report_to_printer_qztray", + ], + "data": [ + "views/pos_config_views.xml", + ], + "assets": { + "point_of_sale._assets_pos": [ + "pos_printing_qztray/static/src/app/printer/qz_tray_connection.esm.js", + "pos_printing_qztray/static/src/app/printer/qz_tray_printer.esm.js", + "pos_printing_qztray/static/src/app/services/qz_tray_printer_service.esm.js", + ], + }, +} diff --git a/pos_printing_qztray/controllers/__init__.py b/pos_printing_qztray/controllers/__init__.py new file mode 100644 index 0000000000..f43232f012 --- /dev/null +++ b/pos_printing_qztray/controllers/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import main diff --git a/pos_printing_qztray/controllers/main.py b/pos_printing_qztray/controllers/main.py new file mode 100644 index 0000000000..cf19606bd2 --- /dev/null +++ b/pos_printing_qztray/controllers/main.py @@ -0,0 +1,263 @@ +import base64 +import io + +from escpos.printer import Dummy +from PIL import Image + +from odoo import http +from odoo.http import request + + +class PosEscposController(http.Controller): + @http.route("/pos/escpos/render-image", type="jsonrpc", auth="user") + def render_any_report(self, png_base64, width_mm=80): + p = Dummy() + try: + img_bytes = base64.b64decode(png_base64) + img = Image.open(io.BytesIO(img_bytes)) + except Exception as e: + return {"error": f"Invalid image: {e}"} + img = img.convert("1") + if width_mm == 58: + target_width = 384 + else: + target_width = 576 + w_percent = target_width / float(img.width) + h_size = int(float(img.height) * float(w_percent)) + img = img.resize((target_width, h_size), Image.LANCZOS) + try: + p.set(align="center") + p.image(img) + p.textln("") + except Exception as e: + return {"error": f"ESC/POS image error: {e}"} + p.cut() + return base64.b64encode(p.output).decode("utf-8") + + @http.route("/pos/escpos/receipt", type="jsonrpc", auth="user") + def render_template(self, order_id): + order = request.env["pos.order"].search( + [("pos_reference", "ilike", "%" + order_id + "%")], limit=1 + ) + if not order.exists(): + return {"error": "Order not found"} + company = order.company_id + currency = order.currency_id + LINE_WIDTH = 46 + p = Dummy() + self._get_logo(p, company, LINE_WIDTH) + self._get_header(p, company) + self._get_order_info(p, order, LINE_WIDTH) + self._get_order_lines(p, order, currency, LINE_WIDTH) + self._get_payments(p, order, currency, LINE_WIDTH) + self._get_tax_detail(p, order, currency, LINE_WIDTH) + self._get_footer(p, order, LINE_WIDTH) + + p.cut() + return base64.b64encode(p.output).decode("utf-8") + + def _table_columns(self, p, text_list, widths, align): + cols = [] + for i, text in enumerate(text_list): + width = widths[min(i, len(widths) - 1)] + alignment = align[min(i, len(align) - 1)] + text = str(text) + + # Recortar si excede + if len(text) > width: + text = text[:width] + + # Alinear + if alignment == "right": + text = text.rjust(width) + elif alignment == "center": + text = text.center(width) + else: + text = text.ljust(width) + + cols.append(text) + + p.textln("".join(cols)) + + def _get_logo(self, p, company, LINE_WIDTH): + if company.logo: + image_data = base64.b64decode(company.logo) + p.set(align="center") + p.image(io.BytesIO(image_data)) + p.textln("") + p.textln("" * LINE_WIDTH) + + def _get_header(self, p, company): + p.set(align="center", bold=True, width=2, height=2) + p.textln(company.name) + if company.vat: + p.textln(company.vat) + p.set(align="center") + if company.phone: + p.textln(request.env._("Tel: %(phone)s", phone=company.phone)) + if company.email: + p.textln(company.email) + if company.website: + p.textln(company.website) + + def _get_order_info(self, p, order, LINE_WIDTH): + p.textln("" * LINE_WIDTH) + p.set(align="center") + p.textln(request.env._("Served by %(name)s", name=order.user_id.name)) + p.textln(order.name) + + if order.tracking_number: + p.set(double_height=True, double_width=True) + p.textln(order.tracking_number) + p.set(normal_textsize=True) + if order.partner_id: + p.textln("" * LINE_WIDTH) + p.set(bold=True) + p.textln(request.env._("CUSTOMER")) + p.set(normal_textsize=True) + p.textln(order.partner_id.display_name) + p.textln(order.partner_id.street or "") + p.textln(f"{order.partner_id.city or ''} {order.partner_id.zip or ''}") + p.textln(order.partner_id.country_id.display_name or "") + p.textln(order.partner_id.vat or "") + p.textln("" * LINE_WIDTH) + + p.textln("" * LINE_WIDTH) + p.textln("" * LINE_WIDTH) + + def _get_order_lines(self, p, order, currency, LINE_WIDTH): + for line in order.lines: + name = line.product_id.display_name[:30] + line_amount = line.price_subtotal_incl + price = f"{line.price_unit:.2f}" + + if currency.position == "after": + line_amount = f"{line_amount:.2f} {currency.symbol}" + price = f"{price} {currency.symbol}" + else: + line_amount = f"{currency.symbol} {line_amount:.2f}" + price = f"{currency.symbol} {price}" + + qty_measure = line.product_id.uom_id.name or request.env._("unit(s)") + spaces = (LINE_WIDTH - 1) - len(name) - len(price) + + p.set(align="left", bold=True) + p.textln(f"{name}{' ' * spaces}{line_amount}") + p.set(bold=False, width=1, height=1) + p.textln( + request.env._( + "%(qty).2f %(um)s x %(price)s", + qty=line.qty, + um=qty_measure, + price=price, + ) + ) + + p.set(align="center") + p.textln("-" * LINE_WIDTH) + + p.set(align="left", bold=True, width=1, height=1) + if currency.position == "after": + amount_total = f"{order.amount_total:.2f} {currency.symbol}" + else: + amount_total = f"{currency.symbol} {order.amount_total:.2f}" + + total_label = request.env._("TOTAL") + spaces = LINE_WIDTH - len(total_label) - len(amount_total) + p.set(custom_size=True, width=1, height=2) + p.textln(f"{total_label}{' ' * spaces}{amount_total}") + p.set(align="left", bold=False, normal_textsize=True) + + def _get_payments(self, p, order, currency, LINE_WIDTH): + payments = [] + change_lines = [] + + for pmt in order.payment_ids: + if pmt.is_change: + change_lines.append(pmt) + else: + payments.append(pmt) + + for pmt in payments: + p.set(align="left", bold=False, width=1, height=1) + name = pmt.payment_method_id.display_name[:30] or "" + + if currency.position == "after": + amt = f"{pmt.amount:.2f} {currency.symbol}" + else: + amt = f"{currency.symbol} {pmt.amount:.2f}" + + spaces = LINE_WIDTH - len(name) - len(amt) + p.textln(f"{name}{' ' * spaces}{amt}") + + for pmt in change_lines: + p.set(bold=True, width=2, height=2) + name = request.env._("CHANGE") + if currency.position == "after": + amt = f"{pmt.amount:.2f} {currency.symbol}" + else: + amt = f"{currency.symbol} {pmt.amount:.2f}" + + spaces = LINE_WIDTH - len(name) - len(amt) + p.textln(f"{name}{' ' * spaces}{amt}") + + p.set(normal_textsize=True) + p.textln("-" * LINE_WIDTH) + + def _get_tax_detail(self, p, order, currency, LINE_WIDTH): + tax_summary = {} + for line in order.lines: + base = line.price_subtotal + total = line.price_subtotal_incl + tax_amount = total - base + + for tax in line.tax_ids_after_fiscal_position: + if tax.name not in tax_summary: + tax_summary[tax.description] = {"base": 0.0, "tax": 0.0} + tax_summary[tax.description]["base"] += base + tax_summary[tax.description]["tax"] += tax_amount + + if not tax_summary: + return + + p.textln("") + p.textln("-" * LINE_WIDTH) + + widths = [20, 12, 12] + align = ["left", "center", "center"] + + for tax_name, amounts in tax_summary.items(): + base = amounts["base"] + tax_amt = amounts["tax"] + + if currency.position == "after": + base_str = f"{base:.2f} {currency.symbol}" + tax_str = f"{tax_amt:.2f} {currency.symbol}" + else: + base_str = f"{currency.symbol} {base:.2f}" + tax_str = f"{currency.symbol} {tax_amt:.2f}" + + row = [ + tax_name[:20], + base_str, + tax_str, + ] + column_titles = [ + request.env._("Tax"), + request.env._("Base"), + request.env._("Tax Amount"), + ] + self._table_columns(p, column_titles, widths, ["left", "center", "center"]) + self._table_columns(p, row, widths, align) + + p.textln("-" * LINE_WIDTH) + + def _get_footer(self, p, order, LINE_WIDTH): + p.set(align="center") + p.textln(order.pos_reference) + p.textln(order.date_order.strftime("%Y-%m-%d %H:%M:%S")) + if order.company_id.point_of_sale_use_ticket_qr_code: + p.textln("") + p.textln(request.env._("Need an invoice? Scan the QR code")) + p.qr(f"{request.httprequest.host_url}pos/receipt/{order.id}", size=8) + p.textln(request.env._("Code: ") + (order.ticket_code or "")) diff --git a/pos_printing_qztray/i18n/es.po b/pos_printing_qztray/i18n/es.po new file mode 100644 index 0000000000..45f9857d05 --- /dev/null +++ b/pos_printing_qztray/i18n/es.po @@ -0,0 +1,150 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_printing_qz +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-11-24 11:52+0000\n" +"PO-Revision-Date: 2025-11-24 11:52+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "%(qty).2f %(um)s x %(price)s" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "Base" +msgstr "B.I." + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "CHANGE" +msgstr "CAMBIO" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "CUSTOMER" +msgstr "CLIENTE" + +#. module: pos_printing_qztray +#: model_terms:ir.ui.view,arch_db:pos_printing_qztray.view_pos_config_form_qztray +msgid "Cashdrawer" +msgstr "Cajón portamonedas" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "Code: " +msgstr "Código: " + +#. module: pos_printing_qztray +#: model:ir.model.fields,field_description:pos_printing_qztray.field_pos_config__display_name +msgid "Display Name" +msgstr "" + +#. module: pos_printing_qztray +#: model:ir.model.fields,field_description:pos_printing_qztray.field_pos_config__is_qztray +msgid "Enable QZ Tray Printing" +msgstr "Activar impresión con QZ Tray" + +#. module: pos_printing_qztray +#: model:ir.model.fields,field_description:pos_printing_qztray.field_pos_config__id +msgid "ID" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "Need an invoice? Scan the QR code" +msgstr "¿Necesita una factura? Escanee el código QR" + +#. module: pos_printing_qztray +#: model:ir.model,name:pos_printing_qztray.model_pos_config +msgid "Point of Sale Configuration" +msgstr "Configuración del TPV" + +#. module: pos_printing_qztray +#: model_terms:ir.ui.view,arch_db:pos_printing_qztray.view_pos_config_form_qztray +msgid "Print receipts directly to a local printer using QZTray" +msgstr "Imprimir recibos directamente a una impresora local usando QZTray" + +#. module: pos_printing_qztray +#: model:ir.model.fields,help:pos_printing_qztray.field_pos_config__iface_qztray_printer_id +msgid "Printer to use for POS receipts when QZ Tray mode is enabled." +msgstr "" +"Impresora a utilizar para los recibos del TPV cuando el modo QZ Tray está " +"activado." + +#. module: pos_printing_qztray +#: model:ir.model.fields,field_description:pos_printing_qztray.field_pos_config__iface_qztray_printer_id +msgid "QZ Tray Printer" +msgstr "" + +#. module: pos_printing_qztray +#: model_terms:ir.ui.view,arch_db:pos_printing_qztray.view_pos_config_form_qztray +msgid "QZTray" +msgstr "" + +#. module: pos_printing_qztray +#: model_terms:ir.ui.view,arch_db:pos_printing_qztray.view_pos_config_form_qztray +msgid "QZTray Receipt Printer" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +#, fuzzy +msgid "Served by %(name)s" +msgstr "Servido por %s" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "TOTAL" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "Tax" +msgstr "Impuesto" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "Tax Amount" +msgstr "Cuota" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "Tel: %(phone)s" +msgstr "" + +#. module: pos_printing_qztray +#: model:ir.model.fields,help:pos_printing_qztray.field_pos_config__is_qztray +msgid "Use QZ Tray instead of IoT Box for printing POS receipts." +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "unit(s)" +msgstr "" + +#~ msgid "Cash" +#~ msgstr "Efectivo" diff --git a/pos_printing_qztray/i18n/it.po b/pos_printing_qztray/i18n/it.po new file mode 100644 index 0000000000..0401a07fd2 --- /dev/null +++ b/pos_printing_qztray/i18n/it.po @@ -0,0 +1,142 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_printing_qztray +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 19.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "%(qty).2f %(um)s x %(price)s" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "Base" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "CHANGE" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "CUSTOMER" +msgstr "" + +#. module: pos_printing_qztray +#: model_terms:ir.ui.view,arch_db:pos_printing_qztray.view_pos_config_form_qztray +msgid "Cashdrawer" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "Code: " +msgstr "" + +#. module: pos_printing_qztray +#: model:ir.model.fields,field_description:pos_printing_qztray.field_pos_config__display_name +msgid "Display Name" +msgstr "" + +#. module: pos_printing_qztray +#: model:ir.model.fields,field_description:pos_printing_qztray.field_pos_config__is_qztray +msgid "Enable QZ Tray Printing" +msgstr "" + +#. module: pos_printing_qztray +#: model:ir.model.fields,field_description:pos_printing_qztray.field_pos_config__id +msgid "ID" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "Need an invoice? Scan the QR code" +msgstr "" + +#. module: pos_printing_qztray +#: model:ir.model,name:pos_printing_qztray.model_pos_config +msgid "Point of Sale Configuration" +msgstr "" + +#. module: pos_printing_qztray +#: model_terms:ir.ui.view,arch_db:pos_printing_qztray.view_pos_config_form_qztray +msgid "Print receipts directly to a local printer using QZTray" +msgstr "" + +#. module: pos_printing_qztray +#: model:ir.model.fields,help:pos_printing_qztray.field_pos_config__iface_qztray_printer_id +msgid "Printer to use for POS receipts when QZ Tray mode is enabled." +msgstr "" + +#. module: pos_printing_qztray +#: model:ir.model.fields,field_description:pos_printing_qztray.field_pos_config__iface_qztray_printer_id +msgid "QZ Tray Printer" +msgstr "" + +#. module: pos_printing_qztray +#: model_terms:ir.ui.view,arch_db:pos_printing_qztray.view_pos_config_form_qztray +msgid "QZTray" +msgstr "" + +#. module: pos_printing_qztray +#: model_terms:ir.ui.view,arch_db:pos_printing_qztray.view_pos_config_form_qztray +msgid "QZTray Receipt Printer" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "Served by %(name)s" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "TOTAL" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "Tax" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "Tax Amount" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "Tel: %(phone)s" +msgstr "" + +#. module: pos_printing_qztray +#: model:ir.model.fields,help:pos_printing_qztray.field_pos_config__is_qztray +msgid "Use QZ Tray instead of IoT Box for printing POS receipts." +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "unit(s)" +msgstr "" diff --git a/pos_printing_qztray/i18n/pos_printing_qztray.pot b/pos_printing_qztray/i18n/pos_printing_qztray.pot new file mode 100644 index 0000000000..f8227cbbfe --- /dev/null +++ b/pos_printing_qztray/i18n/pos_printing_qztray.pot @@ -0,0 +1,141 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_printing_qztray +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 19.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "%(qty).2f %(um)s x %(price)s" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "Base" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "CHANGE" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "CUSTOMER" +msgstr "" + +#. module: pos_printing_qztray +#: model_terms:ir.ui.view,arch_db:pos_printing_qztray.view_pos_config_form_qztray +msgid "Cashdrawer" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "Code: " +msgstr "" + +#. module: pos_printing_qztray +#: model:ir.model.fields,field_description:pos_printing_qztray.field_pos_config__display_name +msgid "Display Name" +msgstr "" + +#. module: pos_printing_qztray +#: model:ir.model.fields,field_description:pos_printing_qztray.field_pos_config__is_qztray +msgid "Enable QZ Tray Printing" +msgstr "" + +#. module: pos_printing_qztray +#: model:ir.model.fields,field_description:pos_printing_qztray.field_pos_config__id +msgid "ID" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "Need an invoice? Scan the QR code" +msgstr "" + +#. module: pos_printing_qztray +#: model:ir.model,name:pos_printing_qztray.model_pos_config +msgid "Point of Sale Configuration" +msgstr "" + +#. module: pos_printing_qztray +#: model_terms:ir.ui.view,arch_db:pos_printing_qztray.view_pos_config_form_qztray +msgid "Print receipts directly to a local printer using QZTray" +msgstr "" + +#. module: pos_printing_qztray +#: model:ir.model.fields,help:pos_printing_qztray.field_pos_config__iface_qztray_printer_id +msgid "Printer to use for POS receipts when QZ Tray mode is enabled." +msgstr "" + +#. module: pos_printing_qztray +#: model:ir.model.fields,field_description:pos_printing_qztray.field_pos_config__iface_qztray_printer_id +msgid "QZ Tray Printer" +msgstr "" + +#. module: pos_printing_qztray +#: model_terms:ir.ui.view,arch_db:pos_printing_qztray.view_pos_config_form_qztray +msgid "QZTray" +msgstr "" + +#. module: pos_printing_qztray +#: model_terms:ir.ui.view,arch_db:pos_printing_qztray.view_pos_config_form_qztray +msgid "QZTray Receipt Printer" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "Served by %(name)s" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "TOTAL" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "Tax" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "Tax Amount" +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "Tel: %(phone)s" +msgstr "" + +#. module: pos_printing_qztray +#: model:ir.model.fields,help:pos_printing_qztray.field_pos_config__is_qztray +msgid "Use QZ Tray instead of IoT Box for printing POS receipts." +msgstr "" + +#. module: pos_printing_qztray +#. odoo-python +#: code:addons/pos_printing_qztray/controllers/main.py:0 +msgid "unit(s)" +msgstr "" diff --git a/pos_printing_qztray/models/__init__.py b/pos_printing_qztray/models/__init__.py new file mode 100644 index 0000000000..c87f24f39a --- /dev/null +++ b/pos_printing_qztray/models/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import pos_config diff --git a/pos_printing_qztray/models/pos_config.py b/pos_printing_qztray/models/pos_config.py new file mode 100644 index 0000000000..d2750c6b25 --- /dev/null +++ b/pos_printing_qztray/models/pos_config.py @@ -0,0 +1,18 @@ +from odoo import fields, models + + +class PosConfig(models.Model): + _inherit = "pos.config" + + is_qztray = fields.Boolean( + string="Enable QZ Tray Printing", + help="Use QZ Tray instead of IoT Box for printing POS receipts.", + default=False, + ) + + iface_qztray_printer_id = fields.Many2one( + comodel_name="printing.printer", + string="QZ Tray Printer", + domain=[("backend", "=", "qztray")], + help="Printer to use for POS receipts when QZ Tray mode is enabled.", + ) diff --git a/pos_printing_qztray/pyproject.toml b/pos_printing_qztray/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/pos_printing_qztray/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/pos_printing_qztray/readme/CONFIGURE.md b/pos_printing_qztray/readme/CONFIGURE.md new file mode 100644 index 0000000000..d5bbd6a94a --- /dev/null +++ b/pos_printing_qztray/readme/CONFIGURE.md @@ -0,0 +1,15 @@ +To configure this module, you need to: + +1. Install and launch the \[QZ Tray\]() desktop + application on the machine where the thermal printer is connected. +2. In Odoo, go to *Point of Sale* \> *Configuration* \> *Settings*. +3. Select your POS configuration and enable **QZ Tray Printing**. +4. Optionally select a **QZ Tray Printer** from the list — only + printers with backend = qztray (managed by + base_report_to_printer_qz) are shown. If left empty, QZ Tray will + use the system default printer. +5. Save the settings and open the POS session. + +To extend or customise the receipt template, inherit PosEscposController +in your own module and override the relevant \_get\_\* methods (e.g. +\_get_header, \_get_order_lines). No frontend changes are required. diff --git a/pos_printing_qztray/readme/CONTRIBUTORS.md b/pos_printing_qztray/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..30642a1803 --- /dev/null +++ b/pos_printing_qztray/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Miquel Alzanillas \ diff --git a/pos_printing_qztray/readme/DESCRIPTION.md b/pos_printing_qztray/readme/DESCRIPTION.md new file mode 100644 index 0000000000..fd2dd36862 --- /dev/null +++ b/pos_printing_qztray/readme/DESCRIPTION.md @@ -0,0 +1,15 @@ +This module provides a pure-software alternative to the Odoo IoT Box for +thermal receipt printing in the Point of Sale, using \[QZ +Tray\]() as the print driver. + +It extends base_report_to_printer_qz — which adds QZ Tray backend +support to printing.printer — and brings that functionality into the +POS, allowing users to select a printer with backend = qztray directly +from the POS configuration. + +Key features: + +- **Cash drawer support**: the cash drawer open command (ESC p) is sent + automatically after each receipt print. +- **No IoT Box required**: designed as a software-only alternative for + setups where deploying IoT infrastructure is not desirable. diff --git a/pos_printing_qztray/readme/INSTALL.md b/pos_printing_qztray/readme/INSTALL.md new file mode 100644 index 0000000000..deb3fa61e2 --- /dev/null +++ b/pos_printing_qztray/readme/INSTALL.md @@ -0,0 +1,13 @@ +This module requires the following non-Python dependency: + +- \[QZ Tray\]() must be installed and running on the + client machine that operates the POS. + +Odoo module dependency (must be installed first): + +- base_report_to_printer_qz — provides the QZ Tray printer backend and + the printing.printer model with backend = qztray support. + +Python dependencies (installed automatically via external_dependencies): + +- python-escpos diff --git a/pos_printing_qztray/readme/ROADMAP.md b/pos_printing_qztray/readme/ROADMAP.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pos_printing_qztray/readme/USAGE.md b/pos_printing_qztray/readme/USAGE.md new file mode 100644 index 0000000000..b73a54fd83 --- /dev/null +++ b/pos_printing_qztray/readme/USAGE.md @@ -0,0 +1,12 @@ +To use this module, you need to: + +1. Open a POS session that has **QZ Tray Printing** enabled. +2. Make sure the QZ Tray desktop application is running on the POS + machine. +3. Process a sale and click **Payment** to print the receipt. + +The receipt is sent directly to the thermal printer via QZ Tray. Receipt +HTML is rendered to PNG and converted to ESC/POS format server-side before +sending following native Odoo logic. The cash drawer is opened automatically after each print if enabled. + +No IoT Box or network printer configuration is required. diff --git a/pos_printing_qztray/readme/newsfragments/.gitkeep b/pos_printing_qztray/readme/newsfragments/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pos_printing_qztray/static/description/icon.png b/pos_printing_qztray/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1dcc49c24f364e9adf0afbc6fc0bac6dbecdeb11 GIT binary patch literal 10254 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 \ No newline at end of file diff --git a/pos_printing_qztray/static/description/index.html b/pos_printing_qztray/static/description/index.html new file mode 100644 index 0000000000..80505e7afa --- /dev/null +++ b/pos_printing_qztray/static/description/index.html @@ -0,0 +1,506 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

POS Printing QZ

+ +

Alpha License: AGPL-3 OCA/pos Translate me on Weblate Try me on Runboat

+

This module provides a pure-software alternative to the Odoo IoT Box for +thermal receipt printing in the Point of Sale, using [QZ +Tray](https://qz.io/) as the print driver.

+

It extends base_report_to_printer_qz — which adds QZ Tray backend +support to printing.printer — and brings that functionality into the +POS, allowing users to select a printer with backend = qztray directly +from the POS configuration.

+

Key features:

+
    +
  • Cash drawer support: the cash drawer open command (ESC p) is sent +automatically after each receipt print.
  • +
  • No IoT Box required: designed as a software-only alternative for +setups where deploying IoT infrastructure is not desirable.
  • +
+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Installation

+

This module requires the following non-Python dependency:

+
    +
  • [QZ Tray](https://qz.io/) must be installed and running on the client +machine that operates the POS.
  • +
+

Odoo module dependency (must be installed first):

+
    +
  • base_report_to_printer_qz — provides the QZ Tray printer backend and +the printing.printer model with backend = qztray support.
  • +
+

Python dependencies (installed automatically via external_dependencies):

+
    +
  • python-escpos
  • +
+
+
+

Configuration

+

To configure this module, you need to:

+
    +
  1. Install and launch the [QZ Tray](https://qz.io/) desktop application +on the machine where the thermal printer is connected.
  2. +
  3. In Odoo, go to Point of Sale > Configuration > Settings.
  4. +
  5. Select your POS configuration and enable QZ Tray Printing.
  6. +
  7. Optionally select a QZ Tray Printer from the list — only printers +with backend = qztray (managed by base_report_to_printer_qz) are +shown. If left empty, QZ Tray will use the system default printer.
  8. +
  9. Save the settings and open the POS session.
  10. +
+

To extend or customise the receipt template, inherit PosEscposController +in your own module and override the relevant _get_* methods (e.g. +_get_header, _get_order_lines). No frontend changes are required.

+
+
+

Usage

+

To use this module, you need to:

+
    +
  1. Open a POS session that has QZ Tray Printing enabled.
  2. +
  3. Make sure the QZ Tray desktop application is running on the POS +machine.
  4. +
  5. Process a sale and click Payment to print the receipt.
  6. +
+

The receipt is sent directly to the thermal printer via QZ Tray. Receipt +HTML is rendered to PNG and converted to ESC/POS format server-side +before sending following native Odoo logic. The cash drawer is opened +automatically after each print if enabled.

+

No IoT Box or network printer configuration is required.

+
+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • APSL Nagarro
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

miquelalzanillas

+

This module is part of the OCA/pos project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/pos_printing_qztray/static/src/app/printer/qz_tray_connection.esm.js b/pos_printing_qztray/static/src/app/printer/qz_tray_connection.esm.js new file mode 100644 index 0000000000..67c90ab508 --- /dev/null +++ b/pos_printing_qztray/static/src/app/printer/qz_tray_connection.esm.js @@ -0,0 +1,74 @@ +/** @odoo-module **/ +/* global */ + +export class QZConnection { + static async _ensureLoaded() { + if (window.qz) return true; + // Fallback: dynamically load qz-tray.js if POS runs in isolated iframe + return new Promise((resolve, reject) => { + const script = document.createElement("script"); + script.src = "/base_report_to_printer_qztray/static/src/lib/qz-tray.js"; + script.type = "text/javascript"; + script.onload = () => { + console.info("[QZ Tray] dynamically loaded for POS iframe"); + resolve(true); + }; + script.onerror = () => { + console.error("[QZ Tray] failed to load"); + reject(new Error("QZ Tray script could not be loaded")); + }; + document.head.appendChild(script); + }); + } + + static async connect(host = null) { + await this._ensureLoaded(); + if (!window.qz) throw new Error("QZ Tray is not available"); + const opts = host ? {host} : {}; + if (!window.qz.websocket.isActive()) { + await window.qz.websocket.connect(opts); + } + } + + static async disconnect() { + if (window.qz?.websocket?.isActive()) { + await window.qz.websocket.disconnect(); + } + } + + static async print(printerName, data, opts = {}) { + await this._ensureLoaded(); + const qz = window.qz; + await qz.security.setCertificatePromise((resolve, reject) => { + fetch("/qz-certificate", { + cache: "no-store", + headers: {"Content-Type": "text/plain"}, + }) + .then((response) => + response + .text() + .then((text) => (response.ok ? resolve(text) : reject(text))) + ) + .catch(reject); + }); + + await qz.security.setSignatureAlgorithm("SHA512"); + await qz.security.setSignaturePromise((toSign) => (resolve, reject) => { + fetch(`/qz-sign-message?request=${toSign}`, { + cache: "no-store", + headers: {"Content-Type": "text/plain"}, + }) + .then((response) => + response + .text() + .then((text) => (response.ok ? resolve(text) : reject(text))) + ) + .catch(reject); + }); + await qz.websocket.connect(); + const config = qz.configs.create(printerName, opts); + await qz.print(config, data); + await qz.websocket.disconnect(); + return true; + } +} diff --git a/pos_printing_qztray/static/src/app/printer/qz_tray_printer.esm.js b/pos_printing_qztray/static/src/app/printer/qz_tray_printer.esm.js new file mode 100644 index 0000000000..5588406522 --- /dev/null +++ b/pos_printing_qztray/static/src/app/printer/qz_tray_printer.esm.js @@ -0,0 +1,94 @@ +/** @odoo-module **/ +/* global */ + +import {htmlToCanvas} from "@point_of_sale/app/services/render_service"; +import {QZConnection} from "./qz_tray_connection.esm"; +import {rpc} from "@web/core/network/rpc"; + +/** + * QZTrayPrinter + * ------------- + * POS printer driver that uses QZ Tray to send ESC/POS commands. + * It relies on the native Odoo receipt rendering flow: + * HTML receipt -> PNG -> ESC/POS image bytes -> QZ Tray. + */ +export class QZTrayPrinter { + constructor(name) { + this.name = name || "QZTray Printer"; + this.type = "qztray"; + } + + /** + * Print a POS receipt element by converting its HTML to ESC/POS. + * @param {HTMLElement} el - The HTML element representing the receipt. + * @returns {Promise<{successful: boolean, message?: object}>} + */ + async printReceipt(el) { + let activeConnection = false; + + try { + const canvas = await htmlToCanvas(el, {addClass: "pos-receipt-print"}); + const pngBase64 = canvas + .toDataURL("image/png") + .replace(/^data:image\/png;base64,/, ""); + const escpos_data = await rpc("/pos/escpos/render-image", { + png_base64: pngBase64, + }); + const cashdrawer = "\x1B\x70\x00\x19\x19"; + activeConnection = true; + await QZConnection.print(this.name, [ + { + type: "raw", + format: "base64", + data: escpos_data, + }, + cashdrawer, + ]); + + return {successful: true}; + } catch (error) { + console.error("[POS][QZTray] Printing error:", error); + return { + successful: false, + message: { + title: "Printing Error", + body: error.message || "Unable to print using QZ Tray.", + }, + }; + } finally { + if (activeConnection) { + try { + await QZConnection.disconnect(); + } catch { + /* Ignore */ + } + } + } + } + + async openCashbox() { + try { + const opencashcommand = "\x1B\x70\x00\x19\x19"; + await QZConnection.print(this.name, [ + {type: "raw", format: "base64", data: opencashcommand}, + opencashcommand, + ]); + return {successful: true}; + } catch (error) { + console.error("[POS][QZTray] ESC/POS Open Chasdrawer error:", error); + return { + successful: false, + message: { + title: "ESC/POS Print Error", + body: error.message || "Could not open chashdrawer via QZ Tray.", + }, + }; + } finally { + try { + await QZConnection.disconnect(); + } catch { + /* Ignore disconnect errors */ + } + } + } +} diff --git a/pos_printing_qztray/static/src/app/services/qz_tray_printer_service.esm.js b/pos_printing_qztray/static/src/app/services/qz_tray_printer_service.esm.js new file mode 100644 index 0000000000..ac9d561ec7 --- /dev/null +++ b/pos_printing_qztray/static/src/app/services/qz_tray_printer_service.esm.js @@ -0,0 +1,45 @@ +/** @odoo-module **/ +/* global */ + +import {posPrinterService} from "@point_of_sale/app/services/pos_printer_service"; +import {QZTrayPrinter} from "../printer/qz_tray_printer.esm"; +import {registry} from "@web/core/registry"; +import {rpc} from "@web/core/network/rpc"; + +export const QZTrayPrinterService = { + dependencies: ["hardware_proxy"], + async start(env, {hardware_proxy}) { + let printerName = "QZTray"; + + try { + const posConfigId = odoo.pos_config_id; + if (posConfigId) { + const result = await rpc("/web/dataset/call_kw", { + model: "pos.config", + method: "read", + args: [[posConfigId], ["iface_qztray_printer_id"]], + kwargs: {}, + }); + + const config = result?.[0]; + if (config?.iface_qztray_printer_id) { + printerName = config.iface_qztray_printer_id[1]; + console.info(`[POS][QZTray] Printer from backend: ${printerName}`); + } + } else { + console.warn("[POS][QZTray] No pos_config_id found in odoo global."); + } + } catch (error) { + console.error("[POS][QZTray] Could not fetch printer config", error); + } + + const device = new QZTrayPrinter(printerName, "escpos"); + hardware_proxy.printer = device; + + console.info(`[POS][QZTray] Printer service initialized: ${printerName}`); + + posPrinterService.start(env, {hardware_proxy}); + }, +}; + +registry.category("services").add("printer.qztray", QZTrayPrinterService); diff --git a/pos_printing_qztray/tests/__init__.py b/pos_printing_qztray/tests/__init__.py new file mode 100644 index 0000000000..6d3d190a6d --- /dev/null +++ b/pos_printing_qztray/tests/__init__.py @@ -0,0 +1,2 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import test_escpos_controller, test_pos_config diff --git a/pos_printing_qztray/tests/test_escpos_controller.py b/pos_printing_qztray/tests/test_escpos_controller.py new file mode 100644 index 0000000000..3f1fb1de24 --- /dev/null +++ b/pos_printing_qztray/tests/test_escpos_controller.py @@ -0,0 +1,55 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import base64 +import io + +from PIL import Image + +from odoo.tests.common import TransactionCase + +from odoo.addons.pos_printing_qztray.controllers.main import PosEscposController + + +def _make_png_base64(width=10, height=10): + """Helper: generate a minimal white PNG encoded as base64.""" + img = Image.new("RGB", (width, height), color=(255, 255, 255)) + buf = io.BytesIO() + img.save(buf, format="PNG") + return base64.b64encode(buf.getvalue()).decode("utf-8") + + +class TestRenderAnyReport(TransactionCase): + """Unit tests for PosEscposController.render_any_report (image → ESC/POS).""" + + def setUp(self): + super().setUp() + self.controller = PosEscposController() + + def test_valid_png_returns_base64_string(self): + result = self.controller.render_any_report(_make_png_base64(), width_mm=80) + self.assertIsInstance(result, str) + # Must be valid base64 + decoded = base64.b64decode(result) + self.assertGreater(len(decoded), 0) + + def test_valid_png_58mm_returns_base64_string(self): + result = self.controller.render_any_report(_make_png_base64(), width_mm=58) + self.assertIsInstance(result, str) + base64.b64decode(result) # should not raise + + def test_invalid_base64_returns_error_dict(self): + result = self.controller.render_any_report("NOT!VALID!BASE64", width_mm=80) + self.assertIsInstance(result, dict) + self.assertIn("error", result) + + def test_valid_base64_not_an_image_returns_error_dict(self): + not_image = base64.b64encode(b"this is not image data").decode("utf-8") + result = self.controller.render_any_report(not_image, width_mm=80) + self.assertIsInstance(result, dict) + self.assertIn("error", result) + + def test_result_contains_escpos_cut_command(self): + """ESC/POS cut command (GS V) must be present in the output.""" + result = self.controller.render_any_report(_make_png_base64(), width_mm=80) + raw = base64.b64decode(result) + # GS V 65 = full cut (0x1d 0x56 0x41) + self.assertIn(b"\x1d\x56", raw) diff --git a/pos_printing_qztray/tests/test_pos_config.py b/pos_printing_qztray/tests/test_pos_config.py new file mode 100644 index 0000000000..a767adb574 --- /dev/null +++ b/pos_printing_qztray/tests/test_pos_config.py @@ -0,0 +1,38 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo.tests.common import TransactionCase + + +class TestPosConfigQzFields(TransactionCase): + """Tests for the pos.config fields added by pos_printing_qz.""" + + def test_is_qztray_field_is_boolean(self): + field = self.env["pos.config"]._fields.get("is_qztray") + self.assertIsNotNone(field, "Field is_qztray must exist on pos.config") + self.assertEqual(field.type, "boolean") + + def test_is_qztray_defaults_false(self): + field = self.env["pos.config"]._fields.get("is_qztray") + # Resolve the default value the same way Odoo does at create time + default_val = field.default + if callable(default_val): + default_val = default_val(self.env["pos.config"]) + self.assertFalse(default_val, "is_qztray must default to False") + + def test_iface_qztray_printer_id_field_is_many2one(self): + field = self.env["pos.config"]._fields.get("iface_qztray_printer_id") + self.assertIsNotNone( + field, "Field iface_qztray_printer_id must exist on pos.config" + ) + self.assertEqual(field.type, "many2one") + + def test_iface_qztray_printer_id_comodel_is_printing_printer(self): + field = self.env["pos.config"]._fields.get("iface_qztray_printer_id") + self.assertEqual( + field.comodel_name, + "printing.printer", + "iface_qztray_printer_id must relate to printing.printer", + ) + + def test_iface_qztray_printer_id_not_required(self): + field = self.env["pos.config"]._fields.get("iface_qztray_printer_id") + self.assertFalse(field.required, "iface_qztray_printer_id must not be required") diff --git a/pos_printing_qztray/views/pos_config_views.xml b/pos_printing_qztray/views/pos_config_views.xml new file mode 100644 index 0000000000..d2168d9e96 --- /dev/null +++ b/pos_printing_qztray/views/pos_config_views.xml @@ -0,0 +1,45 @@ + + + + pos.config.form.qztray + pos.config + + + + + +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
From 14a536fc7e5c4e998f05bddceaf63b314074d07e Mon Sep 17 00:00:00 2001 From: Miquel Alzanillas Date: Wed, 15 Jul 2026 09:26:09 +0200 Subject: [PATCH 2/4] [IMP] pos_printing_qztray: pre-commit auto fixes --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..3cdd91e072 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# generated from manifests external_dependencies +python-escpos From 950caf0b3d6b66e62614aee72c438fabc976a59f Mon Sep 17 00:00:00 2001 From: Miquel Alzanillas Date: Wed, 15 Jul 2026 09:26:55 +0200 Subject: [PATCH 3/4] [MIG] pos_printing_qztray: Migration to 17.0 --- pos_printing_qztray/README.rst | 21 +-- pos_printing_qztray/__manifest__.py | 2 +- pos_printing_qztray/controllers/main.py | 4 +- pos_printing_qztray/i18n/it.po | 142 ------------------ pos_printing_qztray/readme/ROADMAP.md | 0 .../static/description/index.html | 48 +++--- .../src/app/printer/qz_tray_printer.esm.js | 6 +- .../services/qz_tray_printer_service.esm.js | 14 +- 8 files changed, 38 insertions(+), 199 deletions(-) delete mode 100644 pos_printing_qztray/i18n/it.po delete mode 100644 pos_printing_qztray/readme/ROADMAP.md diff --git a/pos_printing_qztray/README.rst b/pos_printing_qztray/README.rst index 08312dc3ac..bca5213402 100644 --- a/pos_printing_qztray/README.rst +++ b/pos_printing_qztray/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - =============== POS Printing QZ =============== @@ -17,17 +13,17 @@ POS Printing QZ .. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png :target: https://odoo-community.org/page/development-status :alt: Alpha -.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpos-lightgray.png?logo=github - :target: https://github.com/OCA/pos/tree/19.0/pos_printing_qztray + :target: https://github.com/OCA/pos/tree/17.0/pos_printing_qztray :alt: OCA/pos .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/pos-19-0/pos-19-0-pos_printing_qztray + :target: https://translation.odoo-community.org/projects/pos-17-0/pos-17-0-pos_printing_qztray :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/builds?repo=OCA/pos&target_branch=19.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/pos&target_branch=17.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -110,18 +106,13 @@ automatically after each print if enabled. No IoT Box or network printer configuration is required. -Known issues / Roadmap -====================== - - - Bug Tracker =========== Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -159,6 +150,6 @@ Current `maintainer `__: |maintainer-miquelalzanillas| -This module is part of the `OCA/pos `_ project on GitHub. +This module is part of the `OCA/pos `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/pos_printing_qztray/__manifest__.py b/pos_printing_qztray/__manifest__.py index 86ef26c47f..ba52072cd1 100644 --- a/pos_printing_qztray/__manifest__.py +++ b/pos_printing_qztray/__manifest__.py @@ -3,7 +3,7 @@ { "name": "POS Printing QZ", "summary": "POS receipt printing via QZ Tray", - "version": "19.0.1.0.0", + "version": "17.0.1.0.0", # see https://odoo-community.org/page/development-status "development_status": "Alpha", "category": "Point Of Sale", diff --git a/pos_printing_qztray/controllers/main.py b/pos_printing_qztray/controllers/main.py index cf19606bd2..96221027bb 100644 --- a/pos_printing_qztray/controllers/main.py +++ b/pos_printing_qztray/controllers/main.py @@ -9,7 +9,7 @@ class PosEscposController(http.Controller): - @http.route("/pos/escpos/render-image", type="jsonrpc", auth="user") + @http.route("/pos/escpos/render-image", type="json", auth="user") def render_any_report(self, png_base64, width_mm=80): p = Dummy() try: @@ -34,7 +34,7 @@ def render_any_report(self, png_base64, width_mm=80): p.cut() return base64.b64encode(p.output).decode("utf-8") - @http.route("/pos/escpos/receipt", type="jsonrpc", auth="user") + @http.route("/pos/escpos/receipt", type="json", auth="user") def render_template(self, order_id): order = request.env["pos.order"].search( [("pos_reference", "ilike", "%" + order_id + "%")], limit=1 diff --git a/pos_printing_qztray/i18n/it.po b/pos_printing_qztray/i18n/it.po deleted file mode 100644 index 0401a07fd2..0000000000 --- a/pos_printing_qztray/i18n/it.po +++ /dev/null @@ -1,142 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * pos_printing_qztray -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 19.0\n" -"Report-Msgid-Bugs-To: \n" -"Last-Translator: Automatically generated\n" -"Language-Team: none\n" -"Language: it\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" - -#. module: pos_printing_qztray -#. odoo-python -#: code:addons/pos_printing_qztray/controllers/main.py:0 -msgid "%(qty).2f %(um)s x %(price)s" -msgstr "" - -#. module: pos_printing_qztray -#. odoo-python -#: code:addons/pos_printing_qztray/controllers/main.py:0 -msgid "Base" -msgstr "" - -#. module: pos_printing_qztray -#. odoo-python -#: code:addons/pos_printing_qztray/controllers/main.py:0 -msgid "CHANGE" -msgstr "" - -#. module: pos_printing_qztray -#. odoo-python -#: code:addons/pos_printing_qztray/controllers/main.py:0 -msgid "CUSTOMER" -msgstr "" - -#. module: pos_printing_qztray -#: model_terms:ir.ui.view,arch_db:pos_printing_qztray.view_pos_config_form_qztray -msgid "Cashdrawer" -msgstr "" - -#. module: pos_printing_qztray -#. odoo-python -#: code:addons/pos_printing_qztray/controllers/main.py:0 -msgid "Code: " -msgstr "" - -#. module: pos_printing_qztray -#: model:ir.model.fields,field_description:pos_printing_qztray.field_pos_config__display_name -msgid "Display Name" -msgstr "" - -#. module: pos_printing_qztray -#: model:ir.model.fields,field_description:pos_printing_qztray.field_pos_config__is_qztray -msgid "Enable QZ Tray Printing" -msgstr "" - -#. module: pos_printing_qztray -#: model:ir.model.fields,field_description:pos_printing_qztray.field_pos_config__id -msgid "ID" -msgstr "" - -#. module: pos_printing_qztray -#. odoo-python -#: code:addons/pos_printing_qztray/controllers/main.py:0 -msgid "Need an invoice? Scan the QR code" -msgstr "" - -#. module: pos_printing_qztray -#: model:ir.model,name:pos_printing_qztray.model_pos_config -msgid "Point of Sale Configuration" -msgstr "" - -#. module: pos_printing_qztray -#: model_terms:ir.ui.view,arch_db:pos_printing_qztray.view_pos_config_form_qztray -msgid "Print receipts directly to a local printer using QZTray" -msgstr "" - -#. module: pos_printing_qztray -#: model:ir.model.fields,help:pos_printing_qztray.field_pos_config__iface_qztray_printer_id -msgid "Printer to use for POS receipts when QZ Tray mode is enabled." -msgstr "" - -#. module: pos_printing_qztray -#: model:ir.model.fields,field_description:pos_printing_qztray.field_pos_config__iface_qztray_printer_id -msgid "QZ Tray Printer" -msgstr "" - -#. module: pos_printing_qztray -#: model_terms:ir.ui.view,arch_db:pos_printing_qztray.view_pos_config_form_qztray -msgid "QZTray" -msgstr "" - -#. module: pos_printing_qztray -#: model_terms:ir.ui.view,arch_db:pos_printing_qztray.view_pos_config_form_qztray -msgid "QZTray Receipt Printer" -msgstr "" - -#. module: pos_printing_qztray -#. odoo-python -#: code:addons/pos_printing_qztray/controllers/main.py:0 -msgid "Served by %(name)s" -msgstr "" - -#. module: pos_printing_qztray -#. odoo-python -#: code:addons/pos_printing_qztray/controllers/main.py:0 -msgid "TOTAL" -msgstr "" - -#. module: pos_printing_qztray -#. odoo-python -#: code:addons/pos_printing_qztray/controllers/main.py:0 -msgid "Tax" -msgstr "" - -#. module: pos_printing_qztray -#. odoo-python -#: code:addons/pos_printing_qztray/controllers/main.py:0 -msgid "Tax Amount" -msgstr "" - -#. module: pos_printing_qztray -#. odoo-python -#: code:addons/pos_printing_qztray/controllers/main.py:0 -msgid "Tel: %(phone)s" -msgstr "" - -#. module: pos_printing_qztray -#: model:ir.model.fields,help:pos_printing_qztray.field_pos_config__is_qztray -msgid "Use QZ Tray instead of IoT Box for printing POS receipts." -msgstr "" - -#. module: pos_printing_qztray -#. odoo-python -#: code:addons/pos_printing_qztray/controllers/main.py:0 -msgid "unit(s)" -msgstr "" diff --git a/pos_printing_qztray/readme/ROADMAP.md b/pos_printing_qztray/readme/ROADMAP.md deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/pos_printing_qztray/static/description/index.html b/pos_printing_qztray/static/description/index.html index 80505e7afa..7a9b32cab2 100644 --- a/pos_printing_qztray/static/description/index.html +++ b/pos_printing_qztray/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +POS Printing QZ -
+
+

POS Printing QZ

- - -Odoo Community Association - -
-

POS Printing QZ

-

Alpha License: AGPL-3 OCA/pos Translate me on Weblate Try me on Runboat

+

Alpha License: AGPL-3 OCA/pos Translate me on Weblate Try me on Runboat

This module provides a pure-software alternative to the Odoo IoT Box for thermal receipt printing in the Point of Sale, using [QZ Tray](https://qz.io/) as the print driver.

@@ -401,18 +396,17 @@

POS Printing QZ

  • Installation
  • Configuration
  • Usage
  • -
  • Known issues / Roadmap
  • -
  • Bug Tracker
  • -
  • Credits
  • -

    Installation

    +

    Installation

    This module requires the following non-Python dependency:

    -

    Configuration

    +

    Configuration

    To configure this module, you need to:

    1. Install and launch the [QZ Tray](https://qz.io/) desktop application @@ -446,7 +440,7 @@

      Configuration

      _get_header, _get_order_lines). No frontend changes are required.

    -

    Usage

    +

    Usage

    To use this module, you need to:

    1. Open a POS session that has QZ Tray Printing enabled.
    2. @@ -460,33 +454,30 @@

      Usage

      automatically after each print if enabled.

      No IoT Box or network printer configuration is required.

    -
    -

    Bug Tracker

    +

    Bug Tracker

    Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -feedback.

    +feedback.

    Do not contact contributors directly about support or help with technical issues.

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • APSL Nagarro
    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association @@ -496,11 +487,10 @@

    Maintainers

    promote its widespread use.

    Current maintainer:

    miquelalzanillas

    -

    This module is part of the OCA/pos project on GitHub.

    +

    This module is part of the OCA/pos project on GitHub.

    You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

    -
    diff --git a/pos_printing_qztray/static/src/app/printer/qz_tray_printer.esm.js b/pos_printing_qztray/static/src/app/printer/qz_tray_printer.esm.js index 5588406522..42474f5ff1 100644 --- a/pos_printing_qztray/static/src/app/printer/qz_tray_printer.esm.js +++ b/pos_printing_qztray/static/src/app/printer/qz_tray_printer.esm.js @@ -1,9 +1,9 @@ /** @odoo-module **/ /* global */ -import {htmlToCanvas} from "@point_of_sale/app/services/render_service"; +import {htmlToCanvas} from "@point_of_sale/app/printer/render_service"; import {QZConnection} from "./qz_tray_connection.esm"; -import {rpc} from "@web/core/network/rpc"; +import {jsonrpc} from "@web/core/network/rpc_service"; /** * QZTrayPrinter @@ -31,7 +31,7 @@ export class QZTrayPrinter { const pngBase64 = canvas .toDataURL("image/png") .replace(/^data:image\/png;base64,/, ""); - const escpos_data = await rpc("/pos/escpos/render-image", { + const escpos_data = await jsonrpc("/pos/escpos/render-image", { png_base64: pngBase64, }); const cashdrawer = "\x1B\x70\x00\x19\x19"; diff --git a/pos_printing_qztray/static/src/app/services/qz_tray_printer_service.esm.js b/pos_printing_qztray/static/src/app/services/qz_tray_printer_service.esm.js index ac9d561ec7..5f15f26593 100644 --- a/pos_printing_qztray/static/src/app/services/qz_tray_printer_service.esm.js +++ b/pos_printing_qztray/static/src/app/services/qz_tray_printer_service.esm.js @@ -1,20 +1,20 @@ /** @odoo-module **/ /* global */ -import {posPrinterService} from "@point_of_sale/app/services/pos_printer_service"; +import {posPrinterService} from "@point_of_sale/app/printer/pos_printer_service"; import {QZTrayPrinter} from "../printer/qz_tray_printer.esm"; import {registry} from "@web/core/registry"; -import {rpc} from "@web/core/network/rpc"; +import {jsonrpc} from "@web/core/network/rpc_service"; export const QZTrayPrinterService = { - dependencies: ["hardware_proxy"], - async start(env, {hardware_proxy}) { + dependencies: ["hardware_proxy", "pos"], + async start(env, {hardware_proxy, pos}) { let printerName = "QZTray"; try { - const posConfigId = odoo.pos_config_id; + const posConfigId = pos?.config?.id; if (posConfigId) { - const result = await rpc("/web/dataset/call_kw", { + const result = await jsonrpc("/web/dataset/call_kw", { model: "pos.config", method: "read", args: [[posConfigId], ["iface_qztray_printer_id"]], @@ -27,7 +27,7 @@ export const QZTrayPrinterService = { console.info(`[POS][QZTray] Printer from backend: ${printerName}`); } } else { - console.warn("[POS][QZTray] No pos_config_id found in odoo global."); + console.warn("[POS][QZTray] No POS config id found in pos service."); } } catch (error) { console.error("[POS][QZTray] Could not fetch printer config", error); From 98b36e207f7640ed91a2b15a0d32b29dd44ef763 Mon Sep 17 00:00:00 2001 From: Miquel Alzanillas Date: Wed, 15 Jul 2026 09:28:12 +0200 Subject: [PATCH 4/4] [DONT MERGE] pos_printing_qztray: temporary test dependency on report-print-send#469 --- test-requirements.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 test-requirements.txt diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000000..87b9221d5b --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +odoo-addon-base-report-to-printer-qztray@git+https://github.com/OCA/report-print-send.git@refs/pull/469/head#subdirectory=base_report_to_printer_qztray