diff --git a/base_report_to_printer_websocket/README.rst b/base_report_to_printer_websocket/README.rst new file mode 100644 index 00000000000..73551c34c30 --- /dev/null +++ b/base_report_to_printer_websocket/README.rst @@ -0,0 +1,157 @@ +=============================== +Report to printer via WebSocket +=============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:de258c1feb02104fc70ac810fb99f19a11714bb4f55aad6173beddebecef3573 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |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%2Freport--print--send-lightgray.png?logo=github + :target: https://github.com/OCA/report-print-send/tree/18.0/base_report_to_printer_websocket + :alt: OCA/report-print-send +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/report-print-send-18-0/report-print-send-18-0-base_report_to_printer_websocket + :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/report-print-send&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends *base_report_to_printer* to send print jobs through +the Odoo Bus (WebSocket) instead of a traditional print server like +CUPS. + +When a report is printed, the module encodes the rendered PDF in Base64 +and sends a ``print_job`` message through the bus to the user configured +on the printer. A client-side listener running as that user receives the +payload and forwards it to the local printer. + +Main features: + +- No external print server required — works over the existing Odoo Bus. +- Sends print jobs as Base64-encoded PDFs via WebSocket. +- Each printer is bound to a specific Odoo user, so jobs are delivered + only to the right client-side agent. +- Compatible with the standard *base_report_to_printer* configuration + (global, per user, per report, per user + report). +- Works with + ``odoo-print-client ``\ \_ + as the client-side agent to receive and print jobs. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +1. Create a printer record in **Settings > Printing > Printers** with + the backend set to **WebSocket**. + +2. Set the **System Name** to the name of the target printer as known by + the client-side listener (e.g. ``MFC-L3750CDW``). Leave it empty to + use the default system printer. + +3. Set the **WebSocket User** to the Odoo user that will run the + ``odoo-print-client`` agent. Print jobs are delivered through the bus + subscription of this user. + +4. Assign the printer as the default globally, per user, or per report + following the standard *base_report_to_printer* workflow. + +5. Install and run the ``odoo-print-client`` agent on the machine + connected to the printer + + :: + + pip install odoo-print-client + odoo-printer --url "https://odoo.example.com" --db "prod" --user "admin" --password "admin" + + See + ``odoo-print-client on PyPI ``\ \_ + for full configuration options. + +Usage +===== + +Once configured, printing works transparently. When a user prints a +report that is set to *Send to Printer* and the assigned printer uses +the **WebSocket** backend, the module will: + +1. Render the report as PDF. + +2. Encode the PDF content in Base64. + +3. Send a ``print_job`` message through the bus to the printer's + configured user with the following payload + + :: + + { + "printer_name": "", + "file_data": "" + } + +The recommended client-side agent is +``odoo-print-client ``\ \_, +which connects as the configured user and forwards jobs to the local +printer. + +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 +------- + +* ForgeFlow +* Dixmit + +Contributors +------------ + +- `ForgeFlow `__: + + - David Jiménez david.jimenez@forgeflow.com + +- `Dixmit `__: + + - Enric Tobella + +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. + +This module is part of the `OCA/report-print-send `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/base_report_to_printer_websocket/__init__.py b/base_report_to_printer_websocket/__init__.py new file mode 100644 index 00000000000..69f7babdfb1 --- /dev/null +++ b/base_report_to_printer_websocket/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models diff --git a/base_report_to_printer_websocket/__manifest__.py b/base_report_to_printer_websocket/__manifest__.py new file mode 100644 index 00000000000..a113941a761 --- /dev/null +++ b/base_report_to_printer_websocket/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2026 ForgeFlow S.L. (https://www.forgeflow.com) +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Report to printer via WebSocket", + "version": "18.0.1.0.0", + "category": "Generic Modules/Base", + "author": "ForgeFlow,Dixmit,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/report-print-send", + "license": "AGPL-3", + "depends": ["base_report_to_printer", "bus"], + "data": [ + "views/printing_printer.xml", + ], + "assets": { + "web.assets_backend": [ + "/base_report_to_printer_websocket/static/src/js/qweb_action_manager.esm.js", + ], + }, + "installable": True, + "application": False, +} diff --git a/base_report_to_printer_websocket/i18n/base_report_to_printer_websocket.pot b/base_report_to_printer_websocket/i18n/base_report_to_printer_websocket.pot new file mode 100644 index 00000000000..c8b1eceef3c --- /dev/null +++ b/base_report_to_printer_websocket/i18n/base_report_to_printer_websocket.pot @@ -0,0 +1,63 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_report_to_printer_websocket +# +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: base_report_to_printer_websocket +#: model:ir.model.fields,field_description:base_report_to_printer_websocket.field_printing_printer__backend +msgid "Backend" +msgstr "" + +#. module: base_report_to_printer_websocket +#. odoo-javascript +#: code:addons/base_report_to_printer_websocket/static/src/js/qweb_action_manager.esm.js:0 +msgid "Could not send print job!" +msgstr "" + +#. module: base_report_to_printer_websocket +#: model:ir.model.fields,field_description:base_report_to_printer_websocket.field_ir_websocket__display_name +#: model:ir.model.fields,field_description:base_report_to_printer_websocket.field_printing_printer__display_name +msgid "Display Name" +msgstr "" + +#. module: base_report_to_printer_websocket +#: model:ir.model.fields,field_description:base_report_to_printer_websocket.field_ir_websocket__id +#: model:ir.model.fields,field_description:base_report_to_printer_websocket.field_printing_printer__id +msgid "ID" +msgstr "" + +#. module: base_report_to_printer_websocket +#: model:ir.model,name:base_report_to_printer_websocket.model_printing_printer +msgid "Logical Printer" +msgstr "" + +#. module: base_report_to_printer_websocket +#. odoo-javascript +#: code:addons/base_report_to_printer_websocket/static/src/js/qweb_action_manager.esm.js:0 +msgid "Print job sent via WebSocket!" +msgstr "" + +#. module: base_report_to_printer_websocket +#: model:ir.model.fields.selection,name:base_report_to_printer_websocket.selection__printing_printer__backend__websocket +msgid "WebSocket" +msgstr "" + +#. module: base_report_to_printer_websocket +#: model:ir.model.fields,field_description:base_report_to_printer_websocket.field_printing_printer__websocket_user_id +msgid "Websocket User" +msgstr "" + +#. module: base_report_to_printer_websocket +#: model:ir.model,name:base_report_to_printer_websocket.model_ir_websocket +msgid "websocket message handling" +msgstr "" diff --git a/base_report_to_printer_websocket/models/__init__.py b/base_report_to_printer_websocket/models/__init__.py new file mode 100644 index 00000000000..e2930959323 --- /dev/null +++ b/base_report_to_printer_websocket/models/__init__.py @@ -0,0 +1,2 @@ +from . import ir_websocket +from . import printing_printer diff --git a/base_report_to_printer_websocket/models/ir_websocket.py b/base_report_to_printer_websocket/models/ir_websocket.py new file mode 100644 index 00000000000..a73bdcd3cdd --- /dev/null +++ b/base_report_to_printer_websocket/models/ir_websocket.py @@ -0,0 +1,24 @@ +# Copyright 2026 ForgeFlow S.L. (https://www.forgeflow.com) +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class IrWebsocket(models.AbstractModel): + _inherit = "ir.websocket" + + def _build_bus_channel_list(self, channels): + websocket_printers = ( + self.env["printing.printer"] + .sudo() + .search( + [ + ("backend", "=", "websocket"), + ("websocket_user_id", "=", self.env.uid), + ] + ) + ) + for printer in websocket_printers: + channels.append(printer) + return super()._build_bus_channel_list(channels) diff --git a/base_report_to_printer_websocket/models/printing_printer.py b/base_report_to_printer_websocket/models/printing_printer.py new file mode 100644 index 00000000000..c0ebeec8ed5 --- /dev/null +++ b/base_report_to_printer_websocket/models/printing_printer.py @@ -0,0 +1,43 @@ +# Copyright 2026 ForgeFlow S.L. (https://www.forgeflow.com) +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +import os +from base64 import b64encode + +from odoo import fields, models + +_logger = logging.getLogger(__name__) + + +class PrintingPrinter(models.Model): + _inherit = "printing.printer" + + backend = fields.Selection( + selection_add=[("websocket", "WebSocket")], + ondelete={"websocket": "cascade"}, + ) + websocket_user_id = fields.Many2one( + "res.users", + ) + + def print_file(self, file_name, report=None, **print_opts): + if self.backend != "websocket": + return super().print_file(file_name, report=report, **print_opts) + self.ensure_one() + with open(file_name, "rb") as f: + content = f.read() + pdf_b64 = b64encode(content).decode("utf-8") + payload = { + "printer_name": self.system_name or "", + "file_data": pdf_b64, + "file_type": print_opts.get("doc_format", "qweb-pdf"), + } + self.env["bus.bus"]._sendone(self, "print_job", payload) + try: + os.remove(file_name) + except OSError as exc: + _logger.warning("Unable to remove temporary file %s: %s", file_name, exc) + _logger.debug("Print job sent via WebSocket to printer '%s'", self.name) + return True diff --git a/base_report_to_printer_websocket/pyproject.toml b/base_report_to_printer_websocket/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/base_report_to_printer_websocket/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/base_report_to_printer_websocket/readme/CONFIGURE.md b/base_report_to_printer_websocket/readme/CONFIGURE.md new file mode 100644 index 00000000000..4d5b987eafb --- /dev/null +++ b/base_report_to_printer_websocket/readme/CONFIGURE.md @@ -0,0 +1,18 @@ +1. Create a printer record in **Settings > Printing > Printers** with the + backend set to **WebSocket**. +2. Set the **System Name** to the name of the target printer as known by + the client-side listener (e.g. ``MFC-L3750CDW``). Leave it empty to + use the default system printer. +3. Set the **WebSocket User** to the Odoo user that will run the + ``odoo-print-client`` agent. Print jobs are delivered through the bus + subscription of this user. +4. Assign the printer as the default globally, per user, or per report + following the standard *base_report_to_printer* workflow. +5. Install and run the ``odoo-print-client`` agent on the machine + connected to the printer + + pip install odoo-print-client + odoo-printer --url "https://odoo.example.com" --db "prod" --user "admin" --password "admin" + + See `odoo-print-client on PyPI `_ + for full configuration options. \ No newline at end of file diff --git a/base_report_to_printer_websocket/readme/CONTRIBUTORS.md b/base_report_to_printer_websocket/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..20e0310b46e --- /dev/null +++ b/base_report_to_printer_websocket/readme/CONTRIBUTORS.md @@ -0,0 +1,6 @@ +- [ForgeFlow](https://forgeflow.com): + - David Jiménez + +- [Dixmit](https://dixmit.com): + - Enric Tobella + diff --git a/base_report_to_printer_websocket/readme/DESCRIPTION.md b/base_report_to_printer_websocket/readme/DESCRIPTION.md new file mode 100644 index 00000000000..ba12f6b3e4d --- /dev/null +++ b/base_report_to_printer_websocket/readme/DESCRIPTION.md @@ -0,0 +1,18 @@ +This module extends *base_report_to_printer* to send print jobs through +the Odoo Bus (WebSocket) instead of a traditional print server like CUPS. + +When a report is printed, the module encodes the rendered PDF in Base64 +and sends a ``print_job`` message through the bus to the user configured +on the printer. A client-side listener running as that user receives +the payload and forwards it to the local printer. + +Main features: + +- No external print server required — works over the existing Odoo Bus. +- Sends print jobs as Base64-encoded PDFs via WebSocket. +- Each printer is bound to a specific Odoo user, so jobs are delivered + only to the right client-side agent. +- Compatible with the standard *base_report_to_printer* configuration + (global, per user, per report, per user + report). +- Works with `odoo-print-client `_ + as the client-side agent to receive and print jobs. \ No newline at end of file diff --git a/base_report_to_printer_websocket/readme/USAGE.md b/base_report_to_printer_websocket/readme/USAGE.md new file mode 100644 index 00000000000..2672dddf6b3 --- /dev/null +++ b/base_report_to_printer_websocket/readme/USAGE.md @@ -0,0 +1,17 @@ +Once configured, printing works transparently. When a user prints a +report that is set to *Send to Printer* and the assigned printer uses the +**WebSocket** backend, the module will: + +1. Render the report as PDF. +2. Encode the PDF content in Base64. +3. Send a ``print_job`` message through the bus to the printer's + configured user with the following payload + + { + "printer_name": "", + "file_data": "" + } + +The recommended client-side agent is +`odoo-print-client `_, +which connects as the configured user and forwards jobs to the local printer. \ No newline at end of file diff --git a/base_report_to_printer_websocket/static/description/icon.png b/base_report_to_printer_websocket/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/base_report_to_printer_websocket/static/description/icon.png differ diff --git a/base_report_to_printer_websocket/static/description/index.html b/base_report_to_printer_websocket/static/description/index.html new file mode 100644 index 00000000000..151b492f86f --- /dev/null +++ b/base_report_to_printer_websocket/static/description/index.html @@ -0,0 +1,505 @@ + + + + + +Report to printer via WebSocket + + + +
+

Report to printer via WebSocket

+ + +

Beta License: AGPL-3 OCA/report-print-send Translate me on Weblate Try me on Runboat

+

This module extends base_report_to_printer to send print jobs through +the Odoo Bus (WebSocket) instead of a traditional print server like +CUPS.

+

When a report is printed, the module encodes the rendered PDF in Base64 +and sends a print_job message through the bus to the user configured +on the printer. A client-side listener running as that user receives the +payload and forwards it to the local printer.

+

Main features:

+
    +
  • No external print server required — works over the existing Odoo Bus.
  • +
  • Sends print jobs as Base64-encoded PDFs via WebSocket.
  • +
  • Each printer is bound to a specific Odoo user, so jobs are delivered +only to the right client-side agent.
  • +
  • Compatible with the standard base_report_to_printer configuration +(global, per user, per report, per user + report).
  • +
  • Works with +odoo-print-client <https://pypi.org/project/odoo-print-client/>_ +as the client-side agent to receive and print jobs.
  • +
+

Table of contents

+ +
+

Configuration

+
    +
  1. Create a printer record in Settings > Printing > Printers with +the backend set to WebSocket.

    +
  2. +
  3. Set the System Name to the name of the target printer as known by +the client-side listener (e.g. MFC-L3750CDW). Leave it empty to +use the default system printer.

    +
  4. +
  5. Set the WebSocket User to the Odoo user that will run the +odoo-print-client agent. Print jobs are delivered through the bus +subscription of this user.

    +
  6. +
  7. Assign the printer as the default globally, per user, or per report +following the standard base_report_to_printer workflow.

    +
  8. +
  9. Install and run the odoo-print-client agent on the machine +connected to the printer

    +
    +pip install odoo-print-client
    +odoo-printer --url "https://odoo.example.com" --db "prod" --user "admin" --password "admin"
    +
    +

    See +odoo-print-client on PyPI <https://pypi.org/project/odoo-print-client/>_ +for full configuration options.

    +
  10. +
+
+
+

Usage

+

Once configured, printing works transparently. When a user prints a +report that is set to Send to Printer and the assigned printer uses +the WebSocket backend, the module will:

+
    +
  1. Render the report as PDF.

    +
  2. +
  3. Encode the PDF content in Base64.

    +
  4. +
  5. Send a print_job message through the bus to the printer’s +configured user with the following payload

    +
    +{
    +    "printer_name": "<system_name of the printer>",
    +    "file_data": "<base64-encoded PDF>"
    +}
    +
    +
  6. +
+

The recommended client-side agent is +odoo-print-client <https://pypi.org/project/odoo-print-client/>_, +which connects as the configured user and forwards jobs to the local +printer.

+
+
+

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

+
    +
  • ForgeFlow
  • +
  • Dixmit
  • +
+
+
+

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.

+

This module is part of the OCA/report-print-send project on GitHub.

+

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

+
+
+
+ + diff --git a/base_report_to_printer_websocket/static/src/js/qweb_action_manager.esm.js b/base_report_to_printer_websocket/static/src/js/qweb_action_manager.esm.js new file mode 100644 index 00000000000..4dbcf587c98 --- /dev/null +++ b/base_report_to_printer_websocket/static/src/js/qweb_action_manager.esm.js @@ -0,0 +1,33 @@ +import {_t} from "@web/core/l10n/translation"; +import {registry} from "@web/core/registry"; + +async function websocketDispatcher(action, env) { + const orm = env.services.orm; + + const print_action = await orm.call( + "ir.actions.report", + "print_action_for_report_name", + [action.report_name], + {context: {force_print_to_client: action.context.force_print_to_client}} + ); + + if (print_action && print_action.action === "server") { + const result = await orm.call( + "ir.actions.report", + "print_document_client_action", + [action.id, action.context.active_ids, action.data] + ); + if (result) { + env.services.notification.add(_t("Print job sent via WebSocket!"), { + type: "success", + }); + return true; + } + env.services.notification.add(_t("Could not send print job!"), { + type: "danger", + }); + } + return false; +} + +registry.category("report.print.backends").add("websocket", websocketDispatcher); diff --git a/base_report_to_printer_websocket/tests/__init__.py b/base_report_to_printer_websocket/tests/__init__.py new file mode 100644 index 00000000000..90c0ca8a645 --- /dev/null +++ b/base_report_to_printer_websocket/tests/__init__.py @@ -0,0 +1,7 @@ +# Copyright 2026 ForgeFlow S.L. (https://www.forgeflow.com) +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_ir_websocket +from . import test_printing_printer +from . import test_report diff --git a/base_report_to_printer_websocket/tests/test_ir_websocket.py b/base_report_to_printer_websocket/tests/test_ir_websocket.py new file mode 100644 index 00000000000..2c51b385e8e --- /dev/null +++ b/base_report_to_printer_websocket/tests/test_ir_websocket.py @@ -0,0 +1,112 @@ +# Copyright 2026 ForgeFlow S.L. (https://www.forgeflow.com) +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import contextlib +from unittest import mock + +from odoo.http import _request_stack +from odoo.tests.common import TransactionCase + + +class TestIrWebsocket(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.server = cls.env["printing.server"].create({}) + cls.ws_user = cls.env["res.users"].create( + { + "name": "WS Test User", + "login": "ws_test_user", + "groups_id": [ + (4, cls.env.ref("base.group_user").id), + ], + } + ) + cls.printer = cls.env["printing.printer"].create( + { + "name": "WS Printer", + "server_id": cls.server.id, + "system_name": "ws_printer", + "backend": "websocket", + "websocket_user_id": cls.ws_user.id, + } + ) + + @contextlib.contextmanager + def _mock_request(self, user): + """Push a mock request onto Odoo's request stack so that the full + ir.websocket inheritance chain (e.g. mail's add_guest_to_context) + finds a usable request object during tests.""" + fake_req = mock.MagicMock() + fake_req.cookies = {} + fake_req.env = self.env(user=user) + fake_req.session.uid = user.id + _request_stack.push(fake_req) + try: + yield fake_req + finally: + _request_stack.pop() + + def _build_channel_list(self, user, channels): + """Call _build_bus_channel_list and return the resulting channels.""" + with self._mock_request(user): + IrWs = self.env["ir.websocket"].with_user(user) + return IrWs._build_bus_channel_list(list(channels)) + + def _get_printer_channels(self, user, channels=None): + """Return only printing.printer records from the bus channel list.""" + result = self._build_channel_list(user, channels or []) + return [ + ch + for ch in result + if hasattr(ch, "_name") and ch._name == "printing.printer" + ] + + def test_assigned_user_gets_printer_channel(self): + """The user assigned on the printer should receive its channel.""" + printer_channels = self._get_printer_channels(self.ws_user) + self.assertIn(self.printer, printer_channels) + + def test_other_user_does_not_get_printer_channel(self): + """A user not assigned on any printer should not receive printer channels.""" + other_user = self.env["res.users"].create( + { + "name": "Other Test User", + "login": "other_test_user", + "groups_id": [ + (4, self.env.ref("base.group_user").id), + ], + } + ) + printer_channels = self._get_printer_channels(other_user) + self.assertNotIn(self.printer, printer_channels) + + def test_multiple_printers_for_same_user(self): + """A user assigned to multiple printers should receive all of them.""" + printer2 = self.env["printing.printer"].create( + { + "name": "WS Printer 2", + "server_id": self.server.id, + "system_name": "ws_printer_2", + "backend": "websocket", + "websocket_user_id": self.ws_user.id, + } + ) + printer_channels = self._get_printer_channels(self.ws_user) + self.assertIn(self.printer, printer_channels) + self.assertIn(printer2, printer_channels) + + def test_non_websocket_printer_not_added(self): + """Printers with a non-websocket backend should not be added.""" + self.env["printing.printer"].create( + { + "name": "CUPS Printer", + "server_id": self.server.id, + "system_name": "cups_printer", + "backend": "cups", + } + ) + printer_channels = self._get_printer_channels(self.ws_user) + self.assertEqual(len(printer_channels), 1) + self.assertEqual(printer_channels[0], self.printer) diff --git a/base_report_to_printer_websocket/tests/test_printing_printer.py b/base_report_to_printer_websocket/tests/test_printing_printer.py new file mode 100644 index 00000000000..277289b3913 --- /dev/null +++ b/base_report_to_printer_websocket/tests/test_printing_printer.py @@ -0,0 +1,99 @@ +# Copyright 2026 ForgeFlow S.L. (https://www.forgeflow.com) +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from base64 import b64encode +from unittest import mock + +from odoo.tests.common import TransactionCase + + +class TestPrintingPrinterWebSocket(TransactionCase): + def setUp(self): + super().setUp() + self.Model = self.env["printing.printer"] + self.server = self.env["printing.server"].create({}) + self.printer_vals = { + "name": "Printer", + "server_id": self.server.id, + "system_name": "Sys Name", + "backend": "websocket", + "websocket_user_id": self.env.user.id, + } + + def new_record(self): + return self.Model.create(self.printer_vals) + + def test_print_document_sends_to_bus(self): + """print_document should encode PDF as base64 and send via bus.""" + printer = self.new_record() + report = self.env["ir.actions.report"].search([], limit=1) + content = b"%PDF-1.4 test content" + with mock.patch.object( + type(self.env["bus.bus"]), + "_sendone", + ) as mock_sendone: + result = printer.print_document(report, content) + self.assertTrue(result) + mock_sendone.assert_called_once() + call_args = mock_sendone.call_args + self.assertEqual(call_args[0][0], printer) + self.assertEqual(call_args[0][1], "print_job") + payload = call_args[0][2] + self.assertEqual(payload["printer_name"], printer.system_name) + self.assertEqual(payload["file_data"], b64encode(content).decode("utf-8")) + + def test_print_document_string_content(self): + """print_document should handle string content by encoding to UTF-8.""" + printer = self.new_record() + report = self.env["ir.actions.report"].search([], limit=1) + content = "string content" + with mock.patch.object( + type(self.env["bus.bus"]), + "_sendone", + ) as mock_sendone: + result = printer.print_document(report, content) + self.assertTrue(result) + payload = mock_sendone.call_args[0][2] + expected_b64 = b64encode(content.encode("utf-8")).decode("utf-8") + self.assertEqual(payload["file_data"], expected_b64) + + def test_print_document_non_websocket_delegates_to_super(self): + """Non-websocket printers should use the cups print_document flow.""" + self.printer_vals["backend"] = "cups" + printer = self.new_record() + report = self.env["ir.actions.report"].search([], limit=1) + with mock.patch.object( + type(self.env["bus.bus"]), + "_sendone", + ) as mock_sendone: + with mock.patch.object( + type(self.env["printing.printer"]), + "print_file", + ): + printer.print_document(report, b"test") + mock_sendone.assert_not_called() + + def test_print_document_empty_system_name(self): + """When system_name is empty, printer_name in payload should be empty.""" + self.printer_vals["system_name"] = "" + printer = self.new_record() + report = self.env["ir.actions.report"].search([], limit=1) + with mock.patch.object( + type(self.env["bus.bus"]), + "_sendone", + ) as mock_sendone: + printer.print_document(report, b"test") + payload = mock_sendone.call_args[0][2] + self.assertEqual(payload["printer_name"], "") + + def test_print_document_sends_to_printer_record(self): + """print_document should send the bus message to the printer record.""" + printer = self.new_record() + report = self.env["ir.actions.report"].search([], limit=1) + with mock.patch.object( + type(self.env["bus.bus"]), + "_sendone", + ) as mock_sendone: + printer.print_document(report, b"test") + self.assertEqual(mock_sendone.call_args[0][0], printer) diff --git a/base_report_to_printer_websocket/tests/test_report.py b/base_report_to_printer_websocket/tests/test_report.py new file mode 100644 index 00000000000..5b992a1bee5 --- /dev/null +++ b/base_report_to_printer_websocket/tests/test_report.py @@ -0,0 +1,100 @@ +# Copyright 2026 ForgeFlow S.L. (https://www.forgeflow.com) +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from unittest import mock + +from odoo.addons.base_report_to_printer.tests.test_report import TestReport + + +class TestReportWebSocket(TestReport): + def new_printer(self): + return self.env["printing.printer"].create( + { + "name": "WebSocket Printer", + "server_id": self.server.id, + "system_name": "ws_printer", + "backend": "websocket", + "websocket_user_id": self.env.user.id, + "default": True, + "status": "available", + } + ) + + def test_render_qweb_pdf_printable(self): + """Override: mock bus._sendone instead of print_document for websocket.""" + with ( + mock.patch.object( + type(self.env["bus.bus"]), + "_sendone", + ) as mock_sendone, + self.assertLogs(level=logging.WARNING), + ): + self.report.property_printing_action_id.action_type = "server" + printer = self.new_printer() + self.report.printing_printer_id = printer + self.report._render_qweb_pdf(self.report.report_name, self.partners.ids) + mock_sendone.assert_called_once() + call_args = mock_sendone.call_args + self.assertEqual(call_args[0][0], printer) + self.assertEqual(call_args[0][1], "print_job") + payload = call_args[0][2] + self.assertIn("file_data", payload) + self.assertIn("printer_name", payload) + + def test_render_qweb_text_printable(self): + """Override: mock bus._sendone instead of print_document for websocket.""" + with ( + mock.patch.object( + type(self.env["bus.bus"]), + "_sendone", + ) as mock_sendone, + self.assertLogs(level=logging.WARNING), + ): + self.report_text.property_printing_action_id.action_type = "server" + printer = self.new_printer() + self.report_text.printing_printer_id = printer + self.report_text._render_qweb_text( + self.report_text.report_name, self.partners.ids + ) + mock_sendone.assert_called_once() + payload = mock_sendone.call_args[0][2] + self.assertIn("file_data", payload) + + def test_print_document_not_printable(self): + """Override: use websocket printer.""" + self.report.printing_printer_id = self.new_printer() + with ( + mock.patch.object( + type(self.env["bus.bus"]), + "_sendone", + ) as mock_sendone, + self.assertLogs(level=logging.WARNING), + ): + self.report.print_document(self.partners.ids) + mock_sendone.assert_called_once() + + def test_print_document_printable(self): + """Override: use websocket printer.""" + self.report.property_printing_action_id.action_type = "server" + self.report.printing_printer_id = self.new_printer() + with ( + mock.patch.object( + type(self.env["bus.bus"]), + "_sendone", + ) as mock_sendone, + self.assertLogs(level=logging.WARNING), + ): + self.report.print_document(self.partners.ids) + mock_sendone.assert_called_once() + + def test_print_document_string(self): + """Override: websocket handles string content directly.""" + with mock.patch.object( + type(self.env["bus.bus"]), + "_sendone", + ) as mock_sendone: + printer = self.new_printer() + printer.print_document("", "test") + mock_sendone.assert_called_once() diff --git a/base_report_to_printer_websocket/views/printing_printer.xml b/base_report_to_printer_websocket/views/printing_printer.xml new file mode 100644 index 00000000000..889b3da0256 --- /dev/null +++ b/base_report_to_printer_websocket/views/printing_printer.xml @@ -0,0 +1,22 @@ + + + + printing.printer.form (in base_report_to_printer_websocket) + + printing.printer + + + + + + +