diff --git a/base_report_to_qz_tray/README.rst b/base_report_to_qz_tray/README.rst new file mode 100644 index 00000000000..e1a57ff340a --- /dev/null +++ b/base_report_to_qz_tray/README.rst @@ -0,0 +1,151 @@ +================= +Report to QZ Tray +================= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/14.0/base_report_to_qz_tray + :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-14-0/report-print-send-14-0-base_report_to_qz_tray + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/144/14.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows users to send reports to a printer attached to the client. + +It adds an optional behaviour on reports to send it directly to a printer. + +* `Send to Client` is the default behaviour providing you a downloadable PDF +* `Send to Printer` prints the report on selected printer + +Report behaviour is defined by settings. + +You will find this option on default user config, on default report +config and on specific config per user per report. + +This allows you to dedicate a specific paper source for example for +preprinted paper such as payment slip. + +Settings can be configured: + +* globally +* per user +* per report +* per user and report + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +To install this module, you need to: + +#. Install QZ Tray on client - https://qz.io/download/ + +#. Install pyOpenSSL + +PyOpenSSL is required to sign messages sent to the clients QZ Tray (and prevent warnings). + +.. code-block:: bash + + sudo pip3 install pyOpenSSL + +#. Generate a certificate + +Take care about "Common Name (e.g. server FQDN or YOUR name) []:" THIS ENTRY IS IMPORTANT, +this should be your Odoo domain name, can be filled in wildcard format an example of this would be \*.my-odoo-domain.com + +.. code-block:: bash + + openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3650-nodes + +#. Set key and certificate on Odoo + +Go to /Settings/Technical/Parameters/System Parameters and set: +- a parameter called qz.certificate with the content of the certificate. +- a parameter called qz.key with the content of the key. + + +#. Install certificate on client QZ Tray. + +Open QZ Tray menu /Advanced/Site Manager and drag and drop cert.pem for your Odoo domain. + +Configuration +============= + +To configure this module, you need to: + +#. Enable the "Printing / Print User" option under access + rights to give users the ability to view the print menu. + + +The jobs will be sent to the printer installed locally with a name matching the QZ Tray Printer. + +Usage +===== + +Guidelines for use: + + * To print a report on a specific printer or tray, you can change + these in *Settings > Printing > Reports* to define default behaviour. + * To print a report on a specific printer and/or tray for a user, you can + change these in *Settings > Printing > Reports* in + *Specific actions per user* + * Users may also select a default action, printer or tray in their preferences. + +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 smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* PESOL + +Contributors +~~~~~~~~~~~~ + +* Angel Moya + +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_qz_tray/__init__.py b/base_report_to_qz_tray/__init__.py new file mode 100644 index 00000000000..a66bea270c9 --- /dev/null +++ b/base_report_to_qz_tray/__init__.py @@ -0,0 +1,5 @@ +# Copyright (C) 2022 PESOL () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models +from . import controllers diff --git a/base_report_to_qz_tray/__manifest__.py b/base_report_to_qz_tray/__manifest__.py new file mode 100644 index 00000000000..ae650b2bcbe --- /dev/null +++ b/base_report_to_qz_tray/__manifest__.py @@ -0,0 +1,26 @@ +# Copyright (C) 2022 PESOL () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Report to QZ Tray", + "version": "16.0.1.0.0", + "category": "Generic Modules/Base", + "author": "PESOL, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/report-print-send", + "license": "AGPL-3", + "depends": ["web", "mail"], + "data": [ + "security/security.xml", + "views/ir_actions_report.xml", + "views/res_users.xml", + ], + "installable": True, + "application": False, + "external_dependencies": {"python": ["pyOpenSSL"]}, + "assets": { + "web.assets_backend": [ + "base_report_to_qz_tray/static/src/lib/qz-tray.js", + "base_report_to_qz_tray/static/src/js/qweb_action_manager.esm.js", + ] + }, +} diff --git a/base_report_to_qz_tray/controllers/__init__.py b/base_report_to_qz_tray/controllers/__init__.py new file mode 100644 index 00000000000..4fa9b4e8e08 --- /dev/null +++ b/base_report_to_qz_tray/controllers/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2022 PESOL () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import main diff --git a/base_report_to_qz_tray/controllers/main.py b/base_report_to_qz_tray/controllers/main.py new file mode 100644 index 00000000000..7596294ac0d --- /dev/null +++ b/base_report_to_qz_tray/controllers/main.py @@ -0,0 +1,29 @@ +# Copyright (C) 2022 PESOL () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import base64 + +from OpenSSL import crypto + +from odoo import http +from odoo.http import request + + +class SignMessage(http.Controller): + @http.route("/qz-certificate/", auth="public") + def qz_certificate(self, **kwargs): + config_param_sudo = request.env["ir.config_parameter"].sudo() + cert = config_param_sudo.get_param("qz.certificate", default=False) + return request.make_response(cert, [("Content-Type", "text/plain")]) + + @http.route("/qz-sign-message/", auth="public") + def qz_sign_message(self, **kwargs): + config_param_sudo = request.env["ir.config_parameter"].sudo() + key = config_param_sudo.get_param("qz.key", default=False) + password = None + pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key, password) + sign = crypto.sign( + pkey, bytes(kwargs.get("request", ""), encoding="utf-8"), "sha512" + ) + data_base64 = base64.b64encode(sign) + return request.make_response(data_base64, [("Content-Type", "text/plain")]) diff --git a/base_report_to_qz_tray/models/__init__.py b/base_report_to_qz_tray/models/__init__.py new file mode 100644 index 00000000000..d0d422cc32b --- /dev/null +++ b/base_report_to_qz_tray/models/__init__.py @@ -0,0 +1,7 @@ +# Copyright (C) 2022 PESOL () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import ir_actions_report +from . import printing_report_qz_tray +from . import res_users +from . import mail_template diff --git a/base_report_to_qz_tray/models/ir_actions_report.py b/base_report_to_qz_tray/models/ir_actions_report.py new file mode 100644 index 00000000000..9323ee936ac --- /dev/null +++ b/base_report_to_qz_tray/models/ir_actions_report.py @@ -0,0 +1,97 @@ +# Copyright (C) 2022 PESOL () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import base64 + +from odoo import api, fields, models + + +class IrActionsReport(models.Model): + _inherit = "ir.actions.report" + + printing_report_qz_tray_ids = fields.One2many( + comodel_name="printing.report.qz.tray", + inverse_name="report_id", + string="Default Actions", + help="This field allows configuring action and printer on a per " "user basis", + ) + + @api.model + def qz_tray_for_report_name(self, report_name): + """Returns if the action is a direct print or pdf + + Called from js + """ + report = self._get_report_from_name(report_name) + + if not report or self.env.context.get("must_skip_send_to_printer"): + return {"action": "download", "printer_name": self.env.user.qz_tray_printer} + + # get values for specific report and user + report_action_values = report.get_default_report_action(user_id=self.env.uid) + if report_action_values: + return report_action_values + + # get values for specific report, for all users + report_action_values = report.get_default_report_action() + if report_action_values: + return report_action_values + + # get values for user or download + return { + "id": report.id, + "action": self.env.user.report_default_action or "download", + "printer_name": self.env.user.qz_tray_printer or False, + } + + def get_default_report_action(self, user_id=False): + self.ensure_one() + printing_act_obj = self.env["printing.report.qz.tray"] + printing_report_qz_tray = printing_act_obj.search( + # [ + # ("report_id", "=", self.id), + # ("user_id", "=", user_id), + # ], + [ + ("report_id", "=", self.id), + "|", + ("user_id", "=", user_id), + ("user_id", "=", False), + ], + limit=1, + ) + if not printing_report_qz_tray: + return False + return { + "id": self.id, + "action": printing_report_qz_tray.action, + "printer_name": printing_report_qz_tray.qz_tray_printer, + } + + def get_qz_tray_data(self, res_ids, report_type="pdf", report_name="", data=None): + if report_type == "pdf": + # Pre V16 + # result = self._render_qweb_pdf(res_ids, data) + # Post V16 + result = self.env["ir.actions.report"]._render_qweb_pdf( + report_name, res_ids, data + ) + data = [ + { + "type": "pixel", + "format": "pdf", + "flavor": "base64", + "data": base64.b64encode(result[0]), + } + ] + elif report_type == "text": + # Pre V16 + # result = self._render_qweb_text(res_ids, data) + # Post V16 + result = self.env["ir.actions.report"]._render_qweb_text( + report_name, res_ids, data + ) + data = [result[0].replace(b"\n", b"").decode("unicode_escape")] + else: + data = [] + return data diff --git a/base_report_to_qz_tray/models/mail_template.py b/base_report_to_qz_tray/models/mail_template.py new file mode 100644 index 00000000000..b51bef36039 --- /dev/null +++ b/base_report_to_qz_tray/models/mail_template.py @@ -0,0 +1,22 @@ +# Copyright (C) 2022 PESOL () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class MailTemplate(models.Model): + _inherit = "mail.template" + + def _generate_template(self, res_ids, fields=None): + # Pre-V17 + return super( + MailTemplate, self.with_context(must_skip_send_to_printer=True) + ).generate_email(res_ids, fields=fields) + # V17 onwards + # return super( + # MailTemplate, self.with_context(must_skip_send_to_printer=True) + # )._generate_template( + # res_ids, + # render_fields=render_fields, + # find_or_create_partners=find_or_create_partners, + # ) diff --git a/base_report_to_qz_tray/models/printing_report_qz_tray.py b/base_report_to_qz_tray/models/printing_report_qz_tray.py new file mode 100644 index 00000000000..05395cf92bc --- /dev/null +++ b/base_report_to_qz_tray/models/printing_report_qz_tray.py @@ -0,0 +1,21 @@ +# Copyright (C) 2022 PESOL () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class PrintingReportQzTray(models.Model): + _name = "printing.report.qz.tray" + _description = "Printing Report Qz Tray" + + report_id = fields.Many2one( + comodel_name="ir.actions.report", + string="Report", + required=True, + ) + user_id = fields.Many2one( + comodel_name="res.users", required=False, ondelete="cascade" + ) + action = fields.Selection([("print", "Print"), ("download", "Download")]) + # TODO: Use js to show local printers and select one + qz_tray_printer = fields.Char(string="QZ Tray Printer") diff --git a/base_report_to_qz_tray/models/res_users.py b/base_report_to_qz_tray/models/res_users.py new file mode 100644 index 00000000000..9e3d33acb73 --- /dev/null +++ b/base_report_to_qz_tray/models/res_users.py @@ -0,0 +1,23 @@ +# Copyright (C) 2022 PESOL () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + report_default_action = fields.Selection( + [("print", "Print"), ("download", "Download")], + default="download", + string="Report Action", + ) + # TODO: Use js to show local printers and select one + qz_tray_printer = fields.Char(string="QZ Tray Printer") + + printing_report_qz_tray_ids = fields.One2many( + comodel_name="printing.report.qz.tray", + inverse_name="user_id", + string="Default Report Actions", + help="This field allows configuring action and printer on a per " "user basis", + ) diff --git a/base_report_to_qz_tray/readme/CONFIGURE.rst b/base_report_to_qz_tray/readme/CONFIGURE.rst new file mode 100644 index 00000000000..7a41c3e4c0d --- /dev/null +++ b/base_report_to_qz_tray/readme/CONFIGURE.rst @@ -0,0 +1,7 @@ +To configure this module, you need to: + +#. Enable the "Printing / Print User" option under access + rights to give users the ability to view the print menu. + + +The jobs will be sent to the printer installed locally with a name matching the QZ Tray Printer. diff --git a/base_report_to_qz_tray/readme/CONTRIBUTORS.rst b/base_report_to_qz_tray/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..a623427aec3 --- /dev/null +++ b/base_report_to_qz_tray/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Angel Moya +* Chris Mann \ No newline at end of file diff --git a/base_report_to_qz_tray/readme/DESCRIPTION.rst b/base_report_to_qz_tray/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..ca81ac6f3fa --- /dev/null +++ b/base_report_to_qz_tray/readme/DESCRIPTION.rst @@ -0,0 +1,21 @@ +This module allows users to send reports to a printer attached to the client. + +It adds an optional behaviour on reports to send it directly to a printer. + +* `Send to Client` is the default behaviour providing you a downloadable PDF +* `Send to Printer` prints the report on selected printer + +Report behaviour is defined by settings. + +You will find this option on default user config, on default report +config and on specific config per user per report. + +This allows you to dedicate a specific paper source for example for +preprinted paper such as payment slip. + +Settings can be configured: + +* globally +* per user +* per report +* per user and report diff --git a/base_report_to_qz_tray/readme/INSTALL.rst b/base_report_to_qz_tray/readme/INSTALL.rst new file mode 100644 index 00000000000..80a44dd3184 --- /dev/null +++ b/base_report_to_qz_tray/readme/INSTALL.rst @@ -0,0 +1,31 @@ +To install this module, you need to: + +#. Install QZ Tray on client - https://qz.io/download/ + +#. Install pyOpenSSL + +PyOpenSSL is required to sign messages sent to the clients QZ Tray (and prevent warnings). + +.. code-block:: bash + + sudo pip3 install pyOpenSSL + +#. Generate a certificate + +Take care about "Common Name (e.g. server FQDN or YOUR name) []:" THIS ENTRY IS IMPORTANT, +this should be your Odoo domain name, can be filled in wildcard format an example of this would be \*.my-odoo-domain.com + +.. code-block:: bash + + openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3650 -nodes + +#. Set key and certificate on Odoo + +Go to /Settings/Technical/Parameters/System Parameters and set: +- a parameter called qz.certificate with the content of the certificate. +- a parameter called qz.key with the content of the key. + + +#. Install certificate on client QZ Tray. + +Open QZ Tray menu /Advanced/Site Manager and drag and drop cert.pem for your Odoo domain. diff --git a/base_report_to_qz_tray/readme/USAGE.rst b/base_report_to_qz_tray/readme/USAGE.rst new file mode 100644 index 00000000000..f866abdef75 --- /dev/null +++ b/base_report_to_qz_tray/readme/USAGE.rst @@ -0,0 +1,8 @@ +Guidelines for use: + + * To print a report on a specific printer or tray, you can change + these in *Settings > Printing > Reports* to define default behaviour. + * To print a report on a specific printer and/or tray for a user, you can + change these in *Settings > Printing > Reports* in + *Specific actions per user* + * Users may also select a default action, printer or tray in their preferences. diff --git a/base_report_to_qz_tray/security/security.xml b/base_report_to_qz_tray/security/security.xml new file mode 100644 index 00000000000..93ef1b8af2e --- /dev/null +++ b/base_report_to_qz_tray/security/security.xml @@ -0,0 +1,34 @@ + + + + QZ Tray / Print User + + + QZ Tray / Print Manager + + + + + + + + + + Printing Report Xml Action Manager + + + + + + + + + Printing Report Xml Action User + + + + + + + + diff --git a/base_report_to_qz_tray/static/description/index.html b/base_report_to_qz_tray/static/description/index.html new file mode 100644 index 00000000000..cf7b9c307c2 --- /dev/null +++ b/base_report_to_qz_tray/static/description/index.html @@ -0,0 +1,492 @@ + + + + + + +Report to QZ Tray + + + +
+

Report to QZ Tray

+ + +

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

+

This module allows users to send reports to a printer attached to the client.

+

It adds an optional behaviour on reports to send it directly to a printer.

+
    +
  • Send to Client is the default behaviour providing you a downloadable PDF
  • +
  • Send to Printer prints the report on selected printer
  • +
+

Report behaviour is defined by settings.

+

You will find this option on default user config, on default report +config and on specific config per user per report.

+

This allows you to dedicate a specific paper source for example for +preprinted paper such as payment slip.

+

Settings can be configured:

+
    +
  • globally
  • +
  • per user
  • +
  • per report
  • +
  • per user and report
  • +
+

Table of contents

+ +
+

Installation

+

To install this module, you need to:

+
    +
  1. Install QZ Tray on client - https://qz.io/download/
  2. +
  3. Install pyOpenSSL
  4. +
+

PyOpenSSL is required to sign messages sent to the clients QZ Tray (and prevent warnings).

+
+sudo pip3 install pyOpenSSL
+
+
    +
  1. Generate a certificate
  2. +
+

Take care about “Common Name (e.g. server FQDN or YOUR name) []:” THIS ENTRY IS IMPORTANT, +this should be your Odoo domain name, can be filled in wildcard format an example of this would be *.my-odoo-domain.com

+
+openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3650-nodes
+
+
    +
  1. Set key and certificate on Odoo
  2. +
+

Go to /Settings/Technical/Parameters/System Parameters and set: +- a parameter called qz.certificate with the content of the certificate. +- a parameter called qz.key with the content of the key.

+
    +
  1. Install certificate on client QZ Tray.
  2. +
+

Open QZ Tray menu /Advanced/Site Manager and drag and drop cert.pem for your Odoo domain.

+
+
+

Configuration

+

To configure this module, you need to:

+
    +
  1. Enable the “Printing / Print User” option under access +rights to give users the ability to view the print menu.
  2. +
+

The jobs will be sent to the printer installed locally with a name matching the QZ Tray Printer.

+
+
+

Usage

+

Guidelines for use:

+
+
    +
  • To print a report on a specific printer or tray, you can change +these in Settings > Printing > Reports to define default behaviour.
  • +
  • To print a report on a specific printer and/or tray for a user, you can +change these in Settings > Printing > Reports in +Specific actions per user
  • +
  • Users may also select a default action, printer or tray in their preferences.
  • +
+
+
+
+

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 smashing it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • PESOL
  • +
+
+
+

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_qz_tray/static/src/js/qweb_action_manager.esm.js b/base_report_to_qz_tray/static/src/js/qweb_action_manager.esm.js new file mode 100644 index 00000000000..09571e5d7f2 --- /dev/null +++ b/base_report_to_qz_tray/static/src/js/qweb_action_manager.esm.js @@ -0,0 +1,109 @@ +/** @odoo-module **/ +/* global qz */ + +import {_t} from "@web/core/l10n/translation"; +import {registry} from "@web/core/registry"; + +export default class PrintActionHandler { + constructor() { + 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); + }); + qz.security.setSignatureAlgorithm("SHA512"); + 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); + }); + } + + async printOrDownloadReport(action, options, env) { + if (action.report_type === "qweb-pdf") { + return this._triggerDownload(action, options, "pdf", env); + } else if ( + action.report_type === "qweb-text" || + action.report_type === "py3o" + ) { + const report_type = action.report_type === "py3o" ? "py3o" : "text"; + return this._triggerDownload(action, options, report_type, env); + } + } + + async _triggerDownload(action, options, report_type, env) { + try { + env.services.ui.block(); + const report_action = await env.services.rpc("/web/dataset/call_kw", { + model: "ir.actions.report", + method: "qz_tray_for_report_name", + args: [[action.report_name]], + kwargs: {}, + }); + if (report_action && report_action.action === "print") { + env.services.ui.unblock(); + const data = await env.services.rpc("/web/dataset/call_kw", { + model: "ir.actions.report", + method: "get_qz_tray_data", + args: [ + report_action.id, + action.context.active_ids, + report_type, + action.report_name, + ], + kwargs: {data: action.data || {}}, + context: action.context || {}, + }); + // CM-Test: If printer name contains an IP, split and use + var printer_name = report_action.printer_name; + if (printer_name.includes("\\")) { + var parts = printer_name.split("\\"); + var server = parts[0]; + var printer = parts[1]; + printer_name = printer; + await qz.websocket.connect({host: server}); + } else { + await qz.websocket.connect(); + } + const qz_printer_name = await qz.printers.find(printer_name); + const config = qz.configs.create(qz_printer_name); + await qz.print(config, data); + await qz.websocket.disconnect(); + env.services.notification.add( + _t("Document sent to the printer: " + qz_printer_name), + {sticky: true, type: "info"} + ); + // Cancel PDF download + return true; + } + env.services.ui.unblock(); + } catch (err) { + env.services.ui.unblock(); + console.log(err); + } + } +} + +const handler = new PrintActionHandler(); + +function print_or_download_report_handler(action, options, env) { + return handler.printOrDownloadReport(action, options, env); +} + +registry + .category("ir.actions.report handlers") + .add("print_or_download_report", print_or_download_report_handler, {sequence: 0}); diff --git a/base_report_to_qz_tray/static/src/lib/qz-tray.js b/base_report_to_qz_tray/static/src/lib/qz-tray.js new file mode 100644 index 00000000000..1514d8a0900 --- /dev/null +++ b/base_report_to_qz_tray/static/src/lib/qz-tray.js @@ -0,0 +1,2848 @@ +'use strict'; + +/** + * @version 2.2.4-SNAPSHOT + * @overview QZ Tray Connector + *

+ * Connects a web client to the QZ Tray software. + * Enables printing and device communication from javascript. + */ +var qz = (function() { + +///// POLYFILLS ///// + + if (!Array.isArray) { + Array.isArray = function(arg) { + return Object.prototype.toString.call(arg) === '[object Array]'; + }; + } + + if (!Number.isInteger) { + Number.isInteger = function(value) { + return typeof value === 'number' && isFinite(value) && Math.floor(value) === value; + }; + } + +///// PRIVATE METHODS ///// + + var _qz = { + VERSION: "2.2.4-SNAPSHOT", //must match @version above + DEBUG: false, + + log: { + /** Debugging messages */ + trace: function() { if (_qz.DEBUG) { console.log.apply(console, arguments); } }, + /** General messages */ + info: function() { console.info.apply(console, arguments); }, + /** General warnings */ + warn: function() { console.warn.apply(console, arguments); }, + /** Debugging errors */ + allay: function() { if (_qz.DEBUG) { console.warn.apply(console, arguments); } }, + /** General errors */ + error: function() { console.error.apply(console, arguments); } + }, + + + //stream types + streams: { + serial: 'SERIAL', usb: 'USB', hid: 'HID', printer: 'PRINTER', file: 'FILE', socket: 'SOCKET' + }, + + + websocket: { + /** The actual websocket object managing the connection. */ + connection: null, + /** Track if a connection attempt is being cancelled. */ + shutdown: false, + + /** Default parameters used on new connections. Override values using options parameter on {@link qz.websocket.connect}. */ + connectConfig: { + host: ["localhost", "localhost.qz.io"], //hosts QZ Tray can be running on + hostIndex: 0, //internal var - index on host array + usingSecure: true, //boolean use of secure protocol + protocol: { + secure: "wss://", //secure websocket + insecure: "ws://" //insecure websocket + }, + port: { + secure: [8181, 8282, 8383, 8484], //list of secure ports QZ Tray could be listening on + insecure: [8182, 8283, 8384, 8485], //list of insecure ports QZ Tray could be listening on + portIndex: 0 //internal var - index on active port array + }, + keepAlive: 60, //time between pings to keep connection alive, in seconds + retries: 0, //number of times to reconnect before failing + delay: 0 //seconds before firing a connection + }, + + setup: { + /** Loop through possible ports to open connection, sets web socket calls that will settle the promise. */ + findConnection: function(config, resolve, reject) { + if (_qz.websocket.shutdown) { + reject(new Error("Connection attempt cancelled by user")); + return; + } + + //force flag if missing ports + if (!config.port.secure.length) { + if (!config.port.insecure.length) { + reject(new Error("No ports have been specified to connect over")); + return; + } else if (config.usingSecure) { + _qz.log.error("No secure ports specified - forcing insecure connection"); + config.usingSecure = false; + } + } else if (!config.port.insecure.length && !config.usingSecure) { + _qz.log.trace("No insecure ports specified - forcing secure connection"); + config.usingSecure = true; + } + + var deeper = function() { + if (_qz.websocket.shutdown) { + //connection attempt was cancelled, bail out + reject(new Error("Connection attempt cancelled by user")); + return; + } + + config.port.portIndex++; + + if ((config.usingSecure && config.port.portIndex >= config.port.secure.length) + || (!config.usingSecure && config.port.portIndex >= config.port.insecure.length)) { + if (config.hostIndex >= config.host.length - 1) { + //give up, all hope is lost + reject(new Error("Unable to establish connection with QZ")); + return; + } else { + config.hostIndex++; + config.port.portIndex = 0; + } + } + + // recursive call until connection established or all ports are exhausted + _qz.websocket.setup.findConnection(config, resolve, reject); + }; + + var address; + if (config.usingSecure) { + address = config.protocol.secure + config.host[config.hostIndex] + ":" + config.port.secure[config.port.portIndex]; + } else { + address = config.protocol.insecure + config.host[config.hostIndex] + ":" + config.port.insecure[config.port.portIndex]; + } + + try { + _qz.log.trace("Attempting connection", address); + _qz.websocket.connection = new _qz.tools.ws(address); + } + catch(err) { + _qz.log.error(err); + deeper(); + return; + } + + if (_qz.websocket.connection != null) { + _qz.websocket.connection.established = false; + + //called on successful connection to qz, begins setup of websocket calls and resolves connect promise after certificate is sent + _qz.websocket.connection.onopen = function(evt) { + if (!_qz.websocket.connection.established) { + _qz.log.trace(evt); + _qz.log.info("Established connection with QZ Tray on " + address); + + _qz.websocket.setup.openConnection({ resolve: resolve, reject: reject }); + + if (config.keepAlive > 0) { + var interval = setInterval(function() { + if (!_qz.tools.isActive() || _qz.websocket.connection.interval !== interval) { + clearInterval(interval); + return; + } + + _qz.websocket.connection.send("ping"); + }, config.keepAlive * 1000); + + _qz.websocket.connection.interval = interval; + } + } + }; + + //called during websocket close during setup + _qz.websocket.connection.onclose = function() { + // Safari compatibility fix to raise error event + if (_qz.websocket.connection && typeof navigator !== 'undefined' && navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1) { + _qz.websocket.connection.onerror(); + } + }; + + //called for errors during setup (such as invalid ports), reject connect promise only if all ports have been tried + _qz.websocket.connection.onerror = function(evt) { + _qz.log.trace(evt); + + _qz.websocket.connection = null; + + deeper(); + }; + } else { + reject(new Error("Unable to create a websocket connection")); + } + }, + + /** Finish setting calls on successful connection, sets web socket calls that won't settle the promise. */ + openConnection: function(openPromise) { + _qz.websocket.connection.established = true; + + //called when an open connection is closed + _qz.websocket.connection.onclose = function(evt) { + _qz.log.trace(evt); + + _qz.websocket.connection = null; + _qz.websocket.callClose(evt); + _qz.log.info("Closed connection with QZ Tray"); + + for(var uid in _qz.websocket.pendingCalls) { + if (_qz.websocket.pendingCalls.hasOwnProperty(uid)) { + _qz.websocket.pendingCalls[uid].reject(new Error("Connection closed before response received")); + } + } + + //if this is set, then an explicit close call was made + if (this.promise != undefined) { + this.promise.resolve(); + } + }; + + //called for any errors with an open connection + _qz.websocket.connection.onerror = function(evt) { + _qz.websocket.callError(evt); + }; + + //send JSON objects to qz + _qz.websocket.connection.sendData = function(obj) { + _qz.log.trace("Preparing object for websocket", obj); + + if (obj.timestamp == undefined) { + obj.timestamp = Date.now(); + if (typeof obj.timestamp !== 'number') { + obj.timestamp = new Date().getTime(); + } + } + if (obj.promise != undefined) { + obj.uid = _qz.websocket.setup.newUID(); + _qz.websocket.pendingCalls[obj.uid] = obj.promise; + } + + // track requesting monitor + obj.position = { + x: typeof screen !== 'undefined' ? ((screen.availWidth || screen.width) / 2) + (screen.left || screen.availLeft || 0) : 0, + y: typeof screen !== 'undefined' ? ((screen.availHeight || screen.height) / 2) + (screen.top || screen.availTop || 0) : 0 + }; + + try { + if (obj.call != undefined && obj.signature == undefined && _qz.security.needsSigned(obj.call)) { + var signObj = { + call: obj.call, + params: obj.params, + timestamp: obj.timestamp + }; + + //make a hashing promise if not already one + var hashing = _qz.tools.hash(_qz.tools.stringify(signObj)); + if (!hashing.then) { + hashing = _qz.tools.promise(function(resolve) { + resolve(hashing); + }); + } + + hashing.then(function(hashed) { + return _qz.security.callSign(hashed); + }).then(function(signature) { + _qz.log.trace("Signature for call", signature); + obj.signature = signature || ""; + obj.signAlgorithm = _qz.security.signAlgorithm; + + _qz.signContent = undefined; + _qz.websocket.connection.send(_qz.tools.stringify(obj)); + }); + } else { + _qz.log.trace("Signature for call", obj.signature); + + //called for pre-signed content and (unsigned) setup calls + _qz.websocket.connection.send(_qz.tools.stringify(obj)); + } + } + catch(err) { + _qz.log.error(err); + + if (obj.promise != undefined) { + obj.promise.reject(err); + delete _qz.websocket.pendingCalls[obj.uid]; + } + } + }; + + //receive message from qz + _qz.websocket.connection.onmessage = function(evt) { + var returned = JSON.parse(evt.data); + + if (returned.uid == null) { + if (returned.type == null) { + //incorrect response format, likely connected to incompatible qz version + _qz.websocket.connection.close(4003, "Connected to incompatible QZ Tray version"); + + } else { + //streams (callbacks only, no promises) + switch(returned.type) { + case _qz.streams.serial: + if (!returned.event) { + returned.event = JSON.stringify({ portName: returned.key, output: returned.data }); + } + + _qz.serial.callSerial(JSON.parse(returned.event)); + break; + case _qz.streams.socket: + _qz.socket.callSocket(JSON.parse(returned.event)); + break; + case _qz.streams.usb: + if (!returned.event) { + returned.event = JSON.stringify({ vendorId: returned.key[0], productId: returned.key[1], output: returned.data }); + } + + _qz.usb.callUsb(JSON.parse(returned.event)); + break; + case _qz.streams.hid: + _qz.hid.callHid(JSON.parse(returned.event)); + break; + case _qz.streams.printer: + _qz.printers.callPrinter(JSON.parse(returned.event)); + break; + case _qz.streams.file: + _qz.file.callFile(JSON.parse(returned.event)); + break; + default: + _qz.log.allay("Cannot determine stream type for callback", returned); + break; + } + } + + return; + } + + _qz.log.trace("Received response from websocket", returned); + + var promise = _qz.websocket.pendingCalls[returned.uid]; + if (promise == undefined) { + _qz.log.allay('No promise found for returned response'); + } else { + if (returned.error != undefined) { + promise.reject(new Error(returned.error)); + } else { + promise.resolve(returned.result); + } + } + + delete _qz.websocket.pendingCalls[returned.uid]; + }; + + + //send up the certificate before making any calls + //also gives the user a chance to deny the connection + function sendCert(cert) { + if (cert === undefined) { cert = null; } + + //websocket setup, query what version is connected + qz.api.getVersion().then(function(version) { + _qz.websocket.connection.version = version; + _qz.websocket.connection.semver = version.toLowerCase().replace(/-rc\./g, "-rc").split(/[\\+\\.-]/g); + for(var i = 0; i < _qz.websocket.connection.semver.length; i++) { + try { + if (i == 3 && _qz.websocket.connection.semver[i].toLowerCase().indexOf("rc") == 0) { + // Handle "rc1" pre-release by negating build info + _qz.websocket.connection.semver[i] = -(_qz.websocket.connection.semver[i].replace(/\D/g, "")); + continue; + } + _qz.websocket.connection.semver[i] = parseInt(_qz.websocket.connection.semver[i]); + } + catch(ignore) {} + + if (_qz.websocket.connection.semver.length < 4) { + _qz.websocket.connection.semver[3] = 0; + } + } + + //algorithm can be declared before a connection, check for incompatibilities now that we have one + _qz.compatible.algorithm(true); + }).then(function() { + _qz.websocket.connection.sendData({ certificate: cert, promise: openPromise }); + }); + } + + _qz.security.callCert().then(sendCert).catch(function(error) { + _qz.log.warn("Failed to get certificate:", error); + + if (_qz.security.rejectOnCertFailure) { + openPromise.reject(error); + } else { + sendCert(null); + } + }); + }, + + /** Generate unique ID used to map a response to a call. */ + newUID: function() { + var len = 6; + return (new Array(len + 1).join("0") + (Math.random() * Math.pow(36, len) << 0).toString(36)).slice(-len) + } + }, + + dataPromise: function(callName, params, signature, signingTimestamp) { + return _qz.tools.promise(function(resolve, reject) { + var msg = { + call: callName, + promise: { resolve: resolve, reject: reject }, + params: params, + signature: signature, + timestamp: signingTimestamp + }; + + _qz.websocket.connection.sendData(msg); + }); + }, + + /** Library of promises awaiting a response, uid -> promise */ + pendingCalls: {}, + + /** List of functions to call on error from the websocket. */ + errorCallbacks: [], + /** Calls all functions registered to listen for errors. */ + callError: function(evt) { + if (Array.isArray(_qz.websocket.errorCallbacks)) { + for(var i = 0; i < _qz.websocket.errorCallbacks.length; i++) { + _qz.websocket.errorCallbacks[i](evt); + } + } else { + _qz.websocket.errorCallbacks(evt); + } + }, + + /** List of function to call on closing from the websocket. */ + closedCallbacks: [], + /** Calls all functions registered to listen for closing. */ + callClose: function(evt) { + if (Array.isArray(_qz.websocket.closedCallbacks)) { + for(var i = 0; i < _qz.websocket.closedCallbacks.length; i++) { + _qz.websocket.closedCallbacks[i](evt); + } + } else { + _qz.websocket.closedCallbacks(evt); + } + } + }, + + + printing: { + /** Default options used for new printer configs. Can be overridden using {@link qz.configs.setDefaults}. */ + defaultConfig: { + //value purposes are explained in the qz.configs.setDefaults docs + + bounds: null, + colorType: 'color', + copies: 1, + density: 0, + duplex: false, + fallbackDensity: null, + interpolation: 'bicubic', + jobName: null, + legacy: false, + margins: 0, + orientation: null, + paperThickness: null, + printerTray: null, + rasterize: false, + rotation: 0, + scaleContent: true, + size: null, + units: 'in', + + forceRaw: false, + encoding: null, + spool: null + } + }, + + + serial: { + /** List of functions called when receiving data from serial connection. */ + serialCallbacks: [], + /** Calls all functions registered to listen for serial events. */ + callSerial: function(streamEvent) { + if (Array.isArray(_qz.serial.serialCallbacks)) { + for(var i = 0; i < _qz.serial.serialCallbacks.length; i++) { + _qz.serial.serialCallbacks[i](streamEvent); + } + } else { + _qz.serial.serialCallbacks(streamEvent); + } + } + }, + + + socket: { + /** List of functions called when receiving data from network socket connection. */ + socketCallbacks: [], + /** Calls all functions registered to listen for network socket events. */ + callSocket: function(socketEvent) { + if (Array.isArray(_qz.socket.socketCallbacks)) { + for(var i = 0; i < _qz.socket.socketCallbacks.length; i++) { + _qz.socket.socketCallbacks[i](socketEvent); + } + } else { + _qz.socket.socketCallbacks(socketEvent); + } + } + }, + + + usb: { + /** List of functions called when receiving data from usb connection. */ + usbCallbacks: [], + /** Calls all functions registered to listen for usb events. */ + callUsb: function(streamEvent) { + if (Array.isArray(_qz.usb.usbCallbacks)) { + for(var i = 0; i < _qz.usb.usbCallbacks.length; i++) { + _qz.usb.usbCallbacks[i](streamEvent); + } + } else { + _qz.usb.usbCallbacks(streamEvent); + } + } + }, + + + hid: { + /** List of functions called when receiving data from hid connection. */ + hidCallbacks: [], + /** Calls all functions registered to listen for hid events. */ + callHid: function(streamEvent) { + if (Array.isArray(_qz.hid.hidCallbacks)) { + for(var i = 0; i < _qz.hid.hidCallbacks.length; i++) { + _qz.hid.hidCallbacks[i](streamEvent); + } + } else { + _qz.hid.hidCallbacks(streamEvent); + } + } + }, + + + printers: { + /** List of functions called when receiving data from printer connection. */ + printerCallbacks: [], + /** Calls all functions registered to listen for printer events. */ + callPrinter: function(streamEvent) { + if (Array.isArray(_qz.printers.printerCallbacks)) { + for(var i = 0; i < _qz.printers.printerCallbacks.length; i++) { + _qz.printers.printerCallbacks[i](streamEvent); + } + } else { + _qz.printers.printerCallbacks(streamEvent); + } + } + }, + + + file: { + /** List of functions called when receiving info regarding file changes. */ + fileCallbacks: [], + /** Calls all functions registered to listen for file events. */ + callFile: function(streamEvent) { + if (Array.isArray(_qz.file.fileCallbacks)) { + for(var i = 0; i < _qz.file.fileCallbacks.length; i++) { + _qz.file.fileCallbacks[i](streamEvent); + } + } else { + _qz.file.fileCallbacks(streamEvent); + } + } + }, + + + security: { + /** Function used to resolve promise when acquiring site's public certificate. */ + certHandler: function(resolve, reject) { reject(); }, + /** Called to create new promise (using {@link _qz.security.certHandler}) for certificate retrieval. */ + callCert: function() { + if (typeof _qz.security.certHandler.then === 'function') { + //already a promise + return _qz.security.certHandler; + } else if (_qz.security.certHandler.constructor.name === "AsyncFunction") { + //already callable as a promise + return _qz.security.certHandler(); + } else { + //turn into a promise + return _qz.tools.promise(_qz.security.certHandler); + } + }, + + /** Function used to create promise resolver when requiring signed calls. */ + signatureFactory: function() { return function(resolve) { resolve(); } }, + /** Called to create new promise (using {@link _qz.security.signatureFactory}) for signed calls. */ + callSign: function(toSign) { + if (_qz.security.signatureFactory.constructor.name === "AsyncFunction") { + //use directly + return _qz.security.signatureFactory(toSign); + } else { + //use in a promise + return _qz.tools.promise(_qz.security.signatureFactory(toSign)); + } + }, + + /** Signing algorithm used on signatures */ + signAlgorithm: "SHA1", + + rejectOnCertFailure: false, + + needsSigned: function(callName) { + const undialoged = [ + "printers.getStatus", + "printers.stopListening", + "usb.isClaimed", + "usb.closeStream", + "usb.releaseDevice", + "hid.stopListening", + "hid.isClaimed", + "hid.closeStream", + "hid.releaseDevice", + "file.stopListening", + "getVersion" + ]; + + return callName != null && undialoged.indexOf(callName) === -1; + } + }, + + + tools: { + /** Create a new promise */ + promise: function(resolver) { + //prefer global object for historical purposes + if (typeof RSVP !== 'undefined') { + return new RSVP.Promise(resolver); + } else if (typeof Promise !== 'undefined') { + return new Promise(resolver); + } else { + _qz.log.error("Promise/A+ support is required. See qz.api.setPromiseType(...)"); + } + }, + + /** Stub for rejecting with an Error from withing a Promise */ + reject: function(error) { + return _qz.tools.promise(function(resolve, reject) { + reject(error); + }); + }, + + stringify: function(object) { + //old versions of prototype affect stringify + var pjson = Array.prototype.toJSON; + delete Array.prototype.toJSON; + + function skipKeys(key, value) { + if (key === "promise") { + return undefined; + } + + return value; + } + + var result = JSON.stringify(object, skipKeys); + + if (pjson) { + Array.prototype.toJSON = pjson; + } + + return result; + }, + + hash: function(data) { + //prefer global object for historical purposes + if (typeof Sha256 !== 'undefined') { + return Sha256.hash(data); + } else { + return _qz.SHA.hash(data); + } + }, + + ws: typeof WebSocket !== 'undefined' ? WebSocket : null, + + absolute: function(loc) { + if (typeof window !== 'undefined' && typeof document.createElement === 'function') { + var a = document.createElement("a"); + a.href = loc; + return a.href; + } else if (typeof exports === 'object') { + //node.js + require('path').resolve(loc); + } + return loc; + }, + + relative: function(data) { + for(var i = 0; i < data.length; i++) { + if (data[i].constructor === Object) { + var absolute = false; + + if (data[i].data && data[i].data.search && data[i].data.search(/data:image\/\w+;base64,/) === 0) { + //upgrade from old base64 behavior + data[i].flavor = "base64"; + data[i].data = data[i].data.replace(/^data:image\/\w+;base64,/, ""); + } else if (data[i].flavor) { + //if flavor is known, we can directly check for absolute flavor types + if (["FILE", "XML"].indexOf(data[i].flavor.toUpperCase()) > -1) { + absolute = true; + } + } else if (data[i].format && ["HTML", "IMAGE", "PDF", "FILE", "XML"].indexOf(data[i].format.toUpperCase()) > -1) { + //if flavor is not known, all valid pixel formats default to file flavor + //previous v2.0 data also used format as what is now flavor, so we check for those values here too + absolute = true; + } else if (data[i].type && ((["PIXEL", "IMAGE", "PDF"].indexOf(data[i].type.toUpperCase()) > -1 && !data[i].format) + || (["HTML", "PDF"].indexOf(data[i].type.toUpperCase()) > -1 && (!data[i].format || data[i].format.toUpperCase() === "FILE")))) { + //if all we know is pixel type, then it is image's file flavor + //previous v2.0 data also used type as what is now format, so we check for those value here too + absolute = true; + } + + if (absolute) { + //change relative links to absolute + data[i].data = _qz.tools.absolute(data[i].data); + } + if (data[i].options && typeof data[i].options.overlay === 'string') { + data[i].options.overlay = _qz.tools.absolute(data[i].options.overlay); + } + } + } + }, + + /** Performs deep copy to target from remaining params */ + extend: function(target) { + //special case when reassigning properties as objects in a deep copy + if (typeof target !== 'object') { + target = {}; + } + + for(var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + if (!source) { continue; } + + for(var key in source) { + if (source.hasOwnProperty(key)) { + if (target === source[key]) { continue; } + + if (source[key] && source[key].constructor && source[key].constructor === Object) { + var clone; + if (Array.isArray(source[key])) { + clone = target[key] || []; + } else { + clone = target[key] || {}; + } + + target[key] = _qz.tools.extend(clone, source[key]); + } else if (source[key] !== undefined) { + target[key] = source[key]; + } + } + } + } + + return target; + }, + + versionCompare: function(major, minor, patch, build) { + if (_qz.tools.assertActive()) { + var semver = _qz.websocket.connection.semver; + if (semver[0] != major) { + return semver[0] - major; + } + if (minor != undefined && semver[1] != minor) { + return semver[1] - minor; + } + if (patch != undefined && semver[2] != patch) { + return semver[2] - patch; + } + if (build != undefined && semver.length > 3 && semver[3] != build) { + return Number.isInteger(semver[3]) && Number.isInteger(build) ? semver[3] - build : semver[3].toString().localeCompare(build.toString()); + } + return 0; + } + }, + + isVersion: function(major, minor, patch, build) { + return _qz.tools.versionCompare(major, minor, patch, build) == 0; + }, + + isActive: function() { + return !_qz.websocket.shutdown && _qz.websocket.connection != null + && (_qz.websocket.connection.readyState === _qz.tools.ws.OPEN + || _qz.websocket.connection.readyState === _qz.tools.ws.CONNECTING); + }, + + assertActive: function() { + if (_qz.tools.isActive()) { + return true; + } + // Promise won't reject on throw; yet better than 'undefined' + throw new Error("A connection to QZ has not been established yet"); + }, + + uint8ArrayToHex: function(uint8) { + return Array.from(uint8) + .map(function(i) { return i.toString(16).padStart(2, '0'); }) + .join(''); + }, + + uint8ArrayToBase64: function(uint8) { + /** + * Adapted from Egor Nepomnyaschih's code under MIT Licence (C) 2020 + * see https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727 + */ + var map = [ + "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", + "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", + "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "/" + ]; + + var result = '', i, l = uint8.length; + for (i = 2; i < l; i += 3) { + result += map[uint8[i - 2] >> 2]; + result += map[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)]; + result += map[((uint8[i - 1] & 0x0F) << 2) | (uint8[i] >> 6)]; + result += map[uint8[i] & 0x3F]; + } + if (i === l + 1) { // 1 octet yet to write + result += map[uint8[i - 2] >> 2]; + result += map[(uint8[i - 2] & 0x03) << 4]; + result += "=="; + } + if (i === l) { // 2 octets yet to write + result += map[uint8[i - 2] >> 2]; + result += map[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)]; + result += map[(uint8[i - 1] & 0x0F) << 2]; + result += "="; + } + return result; + }, + }, + + compatible: { + /** Converts message format to a previous version's */ + data: function(printData) { + // special handling for Uint8Array + for(var i = 0; i < printData.length; i++) { + if (printData[i].constructor === Object && printData[i].data instanceof Uint8Array) { + if (printData[i].flavor) { + var flavor = printData[i].flavor.toString().toUpperCase(); + switch(flavor) { + case 'BASE64': + printData[i].data = _qz.tools.uint8ArrayToBase64(printData[i].data); + break; + case 'HEX': + printData[i].data = _qz.tools.uint8ArrayToHex(printData[i].data); + break; + default: + throw new Error("Uint8Array conversion to '" + flavor + "' is not supported."); + } + } + } + } + + if (_qz.tools.isVersion(2, 0)) { + /* + 2.0.x conversion + ----- + type=pixel -> use format as 2.0 type (unless 'command' format, which forces 2.0 'raw' type) + type=raw -> 2.0 type has to be 'raw' + if format is 'image' -> force 2.0 'image' format, ignore everything else (unsupported in 2.0) + + flavor translates straight to 2.0 format (unless forced to 'raw'/'image') + */ + _qz.log.trace("Converting print data to v2.0 for " + _qz.websocket.connection.version); + for(var i = 0; i < printData.length; i++) { + if (printData[i].constructor === Object) { + if (printData[i].type && printData[i].type.toUpperCase() === "RAW" && printData[i].format && printData[i].format.toUpperCase() === "IMAGE") { + if (printData[i].flavor && printData[i].flavor.toUpperCase() === "BASE64") { + //special case for raw base64 images + printData[i].data = "data:image/compat;base64," + printData[i].data; + } + printData[i].flavor = "IMAGE"; //forces 'image' format when shifting for conversion + } + if ((printData[i].type && printData[i].type.toUpperCase() === "RAW") || (printData[i].format && printData[i].format.toUpperCase() === "COMMAND")) { + printData[i].format = "RAW"; //forces 'raw' type when shifting for conversion + } + + printData[i].type = printData[i].format; + printData[i].format = printData[i].flavor; + delete printData[i].flavor; + } + } + } + }, + + /* Converts config defaults to match previous version */ + config: function(config, dirty) { + if (_qz.tools.isVersion(2, 0)) { + if (!dirty.rasterize) { + config.rasterize = true; + } + } + if(_qz.tools.versionCompare(2, 2) < 0) { + if(config.forceRaw !== 'undefined') { + config.altPrinting = config.forceRaw; + delete config.forceRaw; + } + } + if(_qz.tools.versionCompare(2, 1, 2, 11) < 0) { + if(config.spool) { + if(config.spool.size) { + config.perSpool = config.spool.size; + delete config.spool.size; + } + if(config.spool.end) { + config.endOfDoc = config.spool.end; + delete config.spool.end; + } + delete config.spool; + } + } + return config; + }, + + /** Compat wrapper with previous version **/ + networking: function(hostname, port, signature, signingTimestamp, mappingCallback) { + // Use 2.0 + if (_qz.tools.isVersion(2, 0)) { + return _qz.tools.promise(function(resolve, reject) { + _qz.websocket.dataPromise('websocket.getNetworkInfo', { + hostname: hostname, + port: port + }, signature, signingTimestamp).then(function(data) { + if (typeof mappingCallback !== 'undefined') { + resolve(mappingCallback(data)); + } else { + resolve(data); + } + }, reject); + }); + } + // Wrap 2.1 + return _qz.tools.promise(function(resolve, reject) { + _qz.websocket.dataPromise('networking.device', { + hostname: hostname, + port: port + }, signature, signingTimestamp).then(function(data) { + resolve({ ipAddress: data.ip, macAddress: data.mac }); + }, reject); + }); + }, + + /** Check if QZ version supports chosen algorithm */ + algorithm: function(quiet) { + //if not connected yet we will assume compatibility exists for the time being + if (_qz.tools.isActive()) { + if (_qz.tools.isVersion(2, 0)) { + if (!quiet) { + _qz.log.warn("Connected to an older version of QZ, alternate signature algorithms are not supported"); + } + return false; + } + } + + return true; + } + }, + + /** + * Adapted from Chris Veness's code under MIT Licence (C) 2002 + * see http://www.movable-type.co.uk/scripts/sha256.html + */ + SHA: { + //@formatter:off - keep this block compact + hash: function(msg) { + // add trailing '1' bit (+ 0's padding) to string [§5.1.1] + msg = _qz.SHA._utf8Encode(msg) + String.fromCharCode(0x80); + + // constants [§4.2.2] + var K = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + ]; + // initial hash value [§5.3.1] + var H = [ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 ]; + + // convert string msg into 512-bit/16-integer blocks arrays of ints [§5.2.1] + var l = msg.length / 4 + 2; // length (in 32-bit integers) of msg + ‘1’ + appended length + var N = Math.ceil(l / 16); // number of 16-integer-blocks required to hold 'l' ints + var M = new Array(N); + + for(var i = 0; i < N; i++) { + M[i] = new Array(16); + for(var j = 0; j < 16; j++) { // encode 4 chars per integer, big-endian encoding + M[i][j] = (msg.charCodeAt(i * 64 + j * 4) << 24) | (msg.charCodeAt(i * 64 + j * 4 + 1) << 16) | + (msg.charCodeAt(i * 64 + j * 4 + 2) << 8) | (msg.charCodeAt(i * 64 + j * 4 + 3)); + } // note running off the end of msg is ok 'cos bitwise ops on NaN return 0 + } + // add length (in bits) into final pair of 32-bit integers (big-endian) [§5.1.1] + // note: most significant word would be (len-1)*8 >>> 32, but since JS converts + // bitwise-op args to 32 bits, we need to simulate this by arithmetic operators + M[N-1][14] = ((msg.length - 1) * 8) / Math.pow(2, 32); + M[N-1][14] = Math.floor(M[N-1][14]); + M[N-1][15] = ((msg.length - 1) * 8) & 0xffffffff; + + // HASH COMPUTATION [§6.1.2] + var W = new Array(64); var a, b, c, d, e, f, g, h; + for(var i = 0; i < N; i++) { + // 1 - prepare message schedule 'W' + for(var t = 0; t < 16; t++) { W[t] = M[i][t]; } + for(var t = 16; t < 64; t++) { W[t] = (_qz.SHA._dev1(W[t-2]) + W[t-7] + _qz.SHA._dev0(W[t-15]) + W[t-16]) & 0xffffffff; } + // 2 - initialise working variables a, b, c, d, e, f, g, h with previous hash value + a = H[0]; b = H[1]; c = H[2]; d = H[3]; e = H[4]; f = H[5]; g = H[6]; h = H[7]; + // 3 - main loop (note 'addition modulo 2^32') + for(var t = 0; t < 64; t++) { + var T1 = h + _qz.SHA._sig1(e) + _qz.SHA._ch(e, f, g) + K[t] + W[t]; + var T2 = _qz.SHA._sig0(a) + _qz.SHA._maj(a, b, c); + h = g; g = f; f = e; e = (d + T1) & 0xffffffff; + d = c; c = b; b = a; a = (T1 + T2) & 0xffffffff; + } + // 4 - compute the new intermediate hash value (note 'addition modulo 2^32') + H[0] = (H[0]+a) & 0xffffffff; H[1] = (H[1]+b) & 0xffffffff; H[2] = (H[2]+c) & 0xffffffff; H[3] = (H[3]+d) & 0xffffffff; + H[4] = (H[4]+e) & 0xffffffff; H[5] = (H[5]+f) & 0xffffffff; H[6] = (H[6]+g) & 0xffffffff; H[7] = (H[7]+h) & 0xffffffff; + } + + return _qz.SHA._hexStr(H[0]) + _qz.SHA._hexStr(H[1]) + _qz.SHA._hexStr(H[2]) + _qz.SHA._hexStr(H[3]) + + _qz.SHA._hexStr(H[4]) + _qz.SHA._hexStr(H[5]) + _qz.SHA._hexStr(H[6]) + _qz.SHA._hexStr(H[7]); + }, + + // Rotates right (circular right shift) value x by n positions + _rotr: function(n, x) { return (x >>> n) | (x << (32 - n)); }, + // logical functions + _sig0: function(x) { return _qz.SHA._rotr(2, x) ^ _qz.SHA._rotr(13, x) ^ _qz.SHA._rotr(22, x); }, + _sig1: function(x) { return _qz.SHA._rotr(6, x) ^ _qz.SHA._rotr(11, x) ^ _qz.SHA._rotr(25, x); }, + _dev0: function(x) { return _qz.SHA._rotr(7, x) ^ _qz.SHA._rotr(18, x) ^ (x >>> 3); }, + _dev1: function(x) { return _qz.SHA._rotr(17, x) ^ _qz.SHA._rotr(19, x) ^ (x >>> 10); }, + _ch: function(x, y, z) { return (x & y) ^ (~x & z); }, + _maj: function(x, y, z) { return (x & y) ^ (x & z) ^ (y & z); }, + // note can't use toString(16) as it is implementation-dependant, and in IE returns signed numbers when used on full words + _hexStr: function(n) { var s = "", v; for(var i = 7; i >= 0; i--) { v = (n >>> (i * 4)) & 0xf; s += v.toString(16); } return s; }, + // implementation of deprecated unescape() based on https://cwestblog.com/2011/05/23/escape-unescape-deprecated/ (and comments) + _unescape: function(str) { + return str.replace(/%(u[\da-f]{4}|[\da-f]{2})/gi, function(seq) { + if (seq.length - 1) { + return String.fromCharCode(parseInt(seq.substring(seq.length - 3 ? 2 : 1), 16)) + } else { + var code = seq.charCodeAt(0); + return code < 256 ? "%" + (0 + code.toString(16)).slice(-2).toUpperCase() : "%u" + ("000" + code.toString(16)).slice(-4).toUpperCase() + } + }); + }, + _utf8Encode: function(str) { + return _qz.SHA._unescape(encodeURIComponent(str)); + } + //@formatter:on + }, + }; + + +///// CONFIG CLASS //// + + /** Object to handle configured printer options. */ + function Config(printer, opts) { + + this.config = _qz.tools.extend({}, _qz.printing.defaultConfig); //create a copy of the default options + this._dirtyOpts = {}; //track which config options have changed from the defaults + + /** + * Set the printer assigned to this config. + * @param {string|Object} newPrinter Name of printer. Use object type to specify printing to file or host. + * @param {string} [newPrinter.name] Name of printer to send printing. + * @param {string} [newPrinter.file] DEPRECATED: Name of file to send printing. + * @param {string} [newPrinter.host] IP address or host name to send printing. + * @param {string} [newPrinter.port] Port used by <printer.host>. + */ + this.setPrinter = function(newPrinter) { + if (typeof newPrinter === 'string') { + newPrinter = { name: newPrinter }; + } + + if(newPrinter && newPrinter.file) { + // TODO: Warn for UNC paths too https://github.com/qzind/tray/issues/730 + if(newPrinter.file.indexOf("\\\\") != 0) { + _qz.log.warn("Printing to file is deprecated. See https://github.com/qzind/tray/issues/730"); + } + } + + this.printer = newPrinter; + }; + + /** + * @returns {Object} The printer currently assigned to this config. + */ + this.getPrinter = function() { + return this.printer; + }; + + /** + * Alter any of the printer options currently applied to this config. + * @param newOpts {Object} The options to change. See qz.configs.setDefaults docs for available values. + * + * @see qz.configs.setDefaults + */ + this.reconfigure = function(newOpts) { + for(var key in newOpts) { + if (newOpts[key] !== undefined) { + this._dirtyOpts[key] = true; + } + } + + _qz.tools.extend(this.config, newOpts); + }; + + /** + * @returns {Object} The currently applied options on this config. + */ + this.getOptions = function() { + return _qz.compatible.config(this.config, this._dirtyOpts); + }; + + // init calls for new config object + this.setPrinter(printer); + this.reconfigure(opts); + } + + /** + * Shortcut method for calling qz.print with a particular config. + * @param {Array} data Array of data being sent to the printer. See qz.print docs for available values. + * @param {boolean} [signature] Pre-signed signature of JSON string containing call, params, and timestamp. + * @param {number} [signingTimestamp] Required with signature. Timestamp used with pre-signed content. + * + * @example + * qz.print(myConfig, ...); // OR + * myConfig.print(...); + * + * @see qz.print + */ + Config.prototype.print = function(data, signature, signingTimestamp) { + qz.print(this, data, signature, signingTimestamp); + }; + + +///// PUBLIC METHODS ///// + + /** @namespace qz */ + var qz = { + + /** + * Calls related specifically to the web socket connection. + * @namespace qz.websocket + */ + websocket: { + /** + * Check connection status. Active connection is necessary for other calls to run. + * + * @returns {boolean} If there is an active connection with QZ Tray. + * + * @see connect + * + * @memberof qz.websocket + */ + isActive: function() { + return _qz.tools.isActive(); + }, + + /** + * Call to setup connection with QZ Tray on user's system. + * + * @param {Object} [options] Configuration options for the web socket connection. + * @param {string|Array} [options.host=['localhost', 'localhost.qz.io']] Host running the QZ Tray software. + * @param {Object} [options.port] Config options for ports to cycle. + * @param {Array} [options.port.secure=[8181, 8282, 8383, 8484]] Array of secure (WSS) ports to try + * @param {Array} [options.port.insecure=[8182, 8283, 8384, 8485]] Array of insecure (WS) ports to try + * @param {boolean} [options.usingSecure=true] If the web socket should try to use secure ports for connecting. + * @param {number} [options.keepAlive=60] Seconds between keep-alive pings to keep connection open. Set to 0 to disable. + * @param {number} [options.retries=0] Number of times to reconnect before failing. + * @param {number} [options.delay=0] Seconds before firing a connection. Ignored if options.retries is 0. + * + * @returns {Promise} + * + * @memberof qz.websocket + */ + connect: function(options) { + return _qz.tools.promise(function(resolve, reject) { + if (_qz.websocket.connection) { + const state = _qz.websocket.connection.readyState; + + if (state === _qz.tools.ws.OPEN) { + reject(new Error("An open connection with QZ Tray already exists")); + return; + } else if (state === _qz.tools.ws.CONNECTING) { + reject(new Error("The current connection attempt has not returned yet")); + return; + } else if (state === _qz.tools.ws.CLOSING) { + reject(new Error("Waiting for previous disconnect request to complete")); + return; + } + } + + if (!_qz.tools.ws) { + reject(new Error("WebSocket not supported by this browser")); + return; + } else if (!_qz.tools.ws.CLOSED || _qz.tools.ws.CLOSED == 2) { + reject(new Error("Unsupported WebSocket version detected: HyBi-00/Hixie-76")); + return; + } + + //ensure some form of options exists for value checks + if (options == undefined) { options = {}; } + + //disable secure ports if page is not secure + if (typeof location === 'undefined' || location.protocol !== 'https:') { + //respect forcing secure ports if it is defined, otherwise disable + if (typeof options.usingSecure === 'undefined') { + _qz.log.trace("Disabling secure ports due to insecure page"); + options.usingSecure = false; + } + } + + //ensure any hosts are passed to internals as an array + if (typeof options.host !== 'undefined' && !Array.isArray(options.host)) { + options.host = [options.host]; + } + + _qz.websocket.shutdown = false; //reset state for new connection attempt + var attempt = function(count) { + var tried = false; + var nextAttempt = function() { + if (!tried) { + tried = true; + + if (options && count < options.retries) { + attempt(count + 1); + } else { + _qz.websocket.connection = null; + reject.apply(null, arguments); + } + } + }; + + var delayed = function() { + var config = _qz.tools.extend({}, _qz.websocket.connectConfig, options); + _qz.websocket.setup.findConnection(config, resolve, nextAttempt) + }; + if (count == 0) { + delayed(); // only retries will be called with a delay + } else { + setTimeout(delayed, options.delay * 1000); + } + }; + + attempt(0); + }); + }, + + /** + * Stop any active connection with QZ Tray. + * + * @returns {Promise} + * + * @memberof qz.websocket + */ + disconnect: function() { + return _qz.tools.promise(function(resolve, reject) { + if (_qz.websocket.connection != null) { + if (_qz.tools.isActive()) { + // handles closing both 'connecting' and 'connected' states + _qz.websocket.shutdown = true; + _qz.websocket.connection.promise = { resolve: resolve, reject: reject }; + _qz.websocket.connection.close(); + } else { + reject(new Error("Current connection is still closing")); + } + } else { + reject(new Error("No open connection with QZ Tray")); + } + }); + }, + + /** + * List of functions called for any connections errors outside of an API call.

+ * Also called if {@link websocket#connect} fails to connect. + * + * @param {Function|Array} calls Single or array of Function({Event} event) calls. + * + * @memberof qz.websocket + */ + setErrorCallbacks: function(calls) { + _qz.websocket.errorCallbacks = calls; + }, + + /** + * List of functions called for any connection closing event outside of an API call.

+ * Also called when {@link websocket#disconnect} is called. + * + * @param {Function|Array} calls Single or array of Function({Event} event) calls. + * + * @memberof qz.websocket + */ + setClosedCallbacks: function(calls) { + _qz.websocket.closedCallbacks = calls; + }, + + /** + * @deprecated Since 2.1.0. Please use qz.networking.device() instead + * + * @param {string} [hostname] Hostname to try to connect to when determining network interfaces, defaults to "google.com" + * @param {number} [port] Port to use with custom hostname, defaults to 443 + * @param {string} [signature] Pre-signed signature of hashed JSON string containing call='websocket.getNetworkInfo', params object, and timestamp. + * @param {number} [signingTimestamp] Required with signature. Timestamp used with pre-signed content. + * + * @returns {Promise|Error>} Connected system's network information. + * + * @memberof qz.websocket + */ + getNetworkInfo: _qz.compatible.networking, + + /** + * @returns {Object<{socket: String, host: String, port: Number}>} Details of active websocket connection + * + * @memberof qz.websocket + */ + getConnectionInfo: function() { + if (_qz.tools.assertActive()) { + var url = _qz.websocket.connection.url.split(/[:\/]+/g); + return { socket: url[0], host: url[1], port: +url[2] }; + } + } + }, + + + /** + * Calls related to getting printer information from the connection. + * @namespace qz.printers + */ + printers: { + /** + * @param {string} [signature] Pre-signed signature of hashed JSON string containing call='printers.getDefault, params, and timestamp. + * @param {number} [signingTimestamp] Required with signature. Timestamp used with pre-signed content. + * + * @returns {Promise} Name of the connected system's default printer. + * + * @memberof qz.printers + */ + getDefault: function(signature, signingTimestamp) { + return _qz.websocket.dataPromise('printers.getDefault', null, signature, signingTimestamp); + }, + + /** + * @param {string} [query] Search for a specific printer. All printers are returned if not provided. + * @param {string} [signature] Pre-signed signature of hashed JSON string containing call='printers.find', params, and timestamp. + * @param {number} [signingTimestamp] Required with signature. Timestamp used with pre-signed content. + * + * @returns {Promise|string|Error>} The matched printer name if query is provided. + * Otherwise an array of printer names found on the connected system. + * + * @memberof qz.printers + */ + find: function(query, signature, signingTimestamp) { + return _qz.websocket.dataPromise('printers.find', { query: query }, signature, signingTimestamp); + }, + + /** + * Provides a list, with additional information, for each printer available to QZ. + * + * @returns {Promise|Object|Error>} + * + * @memberof qz.printers + */ + details: function() { + return _qz.websocket.dataPromise('printers.detail'); + }, + + /** + * Start listening for printer status events, such as paper_jam events. + * Reported under the ACTION type in the streamEvent on callbacks. + * + * @returns {Promise} + * @since 2.1.0 + * + * @see qz.printers.setPrinterCallbacks + * + * @param {null|string|Array} printers Printer or list of printers to listen to, null listens to all. + * @param {Object|null} [options] Printer listener options + * @param {null|boolean} [options.jobData=false] Flag indicating if raw spool file content should be return as well as status information (Windows only) + * @param {null|number} [options.maxJobData=-1] Maximum number of bytes to returns for raw spooled file content (Windows only) + * @param {null|string} [options.flavor="plain"] Flavor of data format returned. Valid flavors are [base64 | hex | plain*] (Windows only) + * + * @memberof qz.printers + */ + startListening: function(printers, options) { + if (!Array.isArray(printers)) { + printers = [printers]; + } + var params = { + printerNames: printers + }; + if (options && options.jobData == true) params.jobData = true; + if (options && options.maxJobData) params.maxJobData = options.maxJobData; + if (options && options.flavor) params.flavor = options.flavor; + return _qz.websocket.dataPromise('printers.startListening', params); + }, + + /** + * Clear the queue of a specified printer or printers. Does not delete retained jobs. + * + * @param {string|Object} [options] Name of printer to clear + * @param {string} [options.printerName] Name of printer to clear + * @param {number} [options.jobId] Cancel a job of a specific JobId instead of canceling all. Must include a printerName. + * + * @returns {Promise} + * @since 2.2.4 + * + * @memberof qz.printers + */ + clearQueue: function(options) { + if (typeof options !== 'object') { + options = { + printerName: options + }; + } + return _qz.websocket.dataPromise('printers.clearQueue', options); + }, + + /** + * Stop listening for printer status actions. + * + * @returns {Promise} + * @since 2.1.0 + * + * @see qz.printers.setPrinterCallbacks + * + * @memberof qz.printers + */ + stopListening: function() { + return _qz.websocket.dataPromise('printers.stopListening'); + }, + + /** + * Retrieve current printer status from any active listeners. + * + * @returns {Promise} + * @since 2.1.0 + * + * @see qz.printers.startListening + * + * @memberof qz.printers + */ + getStatus: function() { + return _qz.websocket.dataPromise('printers.getStatus'); + }, + + /** + * List of functions called for any printer status change. + * Event data will contain {string} printerName and {string} status for all types. + * For RECEIVE types, {Array} output (in hexadecimal format). + * For ERROR types, {string} exception. + * For ACTION types, {string} actionType. + * + * @param {Function|Array} calls Single or array of Function({Object} eventData) calls. + * @since 2.1.0 + * + * @memberof qz.printers + */ + setPrinterCallbacks: function(calls) { + _qz.printers.printerCallbacks = calls; + } + }, + + /** + * Calls related to setting up new printer configurations. + * @namespace qz.configs + */ + configs: { + /** + * Default options used by new configs if not overridden. + * Setting a value to NULL will use the printer's default options. + * Updating these will not update the options on any created config. + * + * @param {Object} options Default options used by printer configs if not overridden. + * + * @param {Object} [options.bounds=null] Bounding box rectangle. + * @param {number} [options.bounds.x=0] Distance from left for bounding box starting corner + * @param {number} [options.bounds.y=0] Distance from top for bounding box starting corner + * @param {number} [options.bounds.width=0] Width of bounding box + * @param {number} [options.bounds.height=0] Height of bounding box + * @param {string} [options.colorType='color'] Valid values [color | grayscale | blackwhite] + * @param {number} [options.copies=1] Number of copies to be printed. + * @param {number|Array|Object|Array|string} [options.density=0] Pixel density (DPI, DPMM, or DPCM depending on [options.units]). + * If provided as an array, uses the first supported density found (or the first entry if none found). + * If provided as a string, valid values are [best | draft], corresponding to highest or lowest reported density respectively. + * @param {number} [options.density.cross=0] Asymmetric pixel density for the cross feed direction. + * @param {number} [options.density.feed=0] Asymmetric pixel density for the feed direction. + * @param {boolean|string} [options.duplex=false] Double sided printing, Can specify duplex style by passing a string value: [one-sided | duplex | long-edge | tumble | short-edge] + * @param {number} [options.fallbackDensity=null] Value used when default density value cannot be read, or in cases where reported as "Normal" by the driver, (in DPI, DPMM, or DPCM depending on [options.units]). + * @param {string} [options.interpolation='bicubic'] Valid values [bicubic | bilinear | nearest-neighbor]. Controls how images are handled when resized. + * @param {string} [options.jobName=null] Name to display in print queue. + * @param {boolean} [options.legacy=false] If legacy style printing should be used. + * @param {Object|number} [options.margins=0] If just a number is provided, it is used as the margin for all sides. + * @param {number} [options.margins.top=0] + * @param {number} [options.margins.right=0] + * @param {number} [options.margins.bottom=0] + * @param {number} [options.margins.left=0] + * @param {string} [options.orientation=null] Valid values [portrait | landscape | reverse-landscape | null]. + * If set to null, orientation will be determined automatically. + * @param {number} [options.paperThickness=null] + * @param {string|number} [options.printerTray=null] Printer tray to pull from. The number N assumes string equivalent of 'Tray N'. Uses printer default if NULL. + * @param {boolean} [options.rasterize=false] Whether documents should be rasterized before printing. + * Specifying [options.density] for PDF print formats will set this to true. + * @param {number} [options.rotation=0] Image rotation in degrees. + * @param {boolean} [options.scaleContent=true] Scales print content to page size, keeping ratio. + * @param {Object} [options.size=null] Paper size. + * @param {number} [options.size.width=null] Page width. + * @param {number} [options.size.height=null] Page height. + * @param {string} [options.units='in'] Page units, applies to paper size, margins, and density. Valid value [in | cm | mm] + * + * @param {boolean} [options.forceRaw=false] Print the specified raw data using direct method, skipping the driver. Not yet supported on Windows. + * @param {string|Object} [options.encoding=null] Character set for commands. Can be provided as an object for converting encoding types for RAW types. + * @param {string} [options.encoding.from] If this encoding type is provided, RAW type commands will be parsed from this for the purpose of being converted to the encoding.to value. + * @param {string} [options.encoding.to] Encoding RAW type commands will be converted into. If encoding.from is not provided, this will be treated as if a string was passed for encoding. + * @param {string} [options.endOfDoc=null] DEPRECATED Raw only: Character(s) denoting end of a page to control spooling. + * @param {number} [options.perSpool=1] DEPRECATED: Raw only: Number of pages per spool. + * @param {boolean} [options.retainTemp=false] Retain any temporary files used. Ignored unless forceRaw true. + * @param {Object} [options.spool=null] Advanced spooling options. + * @param {number} [options.spool.size=null] Number of pages per spool. Default is no limit. If spool.end is provided, defaults to 1 + * @param {string} [options.spool.end=null] Raw only: Character(s) denoting end of a page to control spooling. + * + * @memberof qz.configs + */ + setDefaults: function(options) { + _qz.tools.extend(_qz.printing.defaultConfig, options); + }, + + /** + * Creates new printer config to be used in printing. + * + * @param {string|object} printer Name of printer. Use object type to specify printing to file or host. + * @param {string} [printer.name] Name of printer to send printing. + * @param {string} [printer.file] Name of file to send printing. + * @param {string} [printer.host] IP address or host name to send printing. + * @param {string} [printer.port] Port used by <printer.host>. + * @param {Object} [options] Override any of the default options for this config only. + * + * @returns {Config} The new config. + * + * @see configs.setDefaults + * + * @memberof qz.configs + */ + create: function(printer, options) { + return new Config(printer, options); + } + }, + + + /** + * Send data to selected config for printing. + * The promise for this method will resolve when the document has been sent to the printer. Actual printing may not be complete. + *

+ * Optionally, print requests can be pre-signed: + * Signed content consists of a JSON object string containing no spacing, + * following the format of the "call" and "params" keys in the API call, with the addition of a "timestamp" key in milliseconds + * ex. '{"call":"","params":{...},"timestamp":1450000000}' + * + * @param {Object|Array>} configs Previously created config object or objects. + * @param {Array|Array>} data Array of data being sent to the printer.
+ * String values are interpreted as {type: 'raw', format: 'command', flavor: 'plain', data: <string>}. + * @param {string} data.data + * @param {string} data.type Printing type. Valid types are [pixel | raw*]. *Default + * @param {string} data.format Format of data type used. *Default per type

+ * For [pixel] types, valid formats are [html | image* | pdf].

+ * For [raw] types, valid formats are [command* | html | image | pdf]. + * @param {string} data.flavor Flavor of data format used. *Default per format

+ * For [command] formats, valid flavors are [base64 | file | hex | plain* | xml].

+ * For [html] formats, valid flavors are [file* | plain].

+ * For [image] formats, valid flavors are [base64 | file*].

+ * For [pdf] formats, valid flavors are [base64 | file*]. + * @param {Object} [data.options] + * @param {string} [data.options.language] Required with [raw] type + [image] format. Printer language. + * @param {number} [data.options.x] Optional with [raw] type + [image] format. The X position of the image. + * @param {number} [data.options.y] Optional with [raw] type + [image] format. The Y position of the image. + * @param {string|number} [data.options.dotDensity] Optional with [raw] type + [image] format. + * @param {number} [data.precision=128] Optional with [raw] type [image] format. Bit precision of the ribbons. + * @param {boolean|string|Array>} [data.options.overlay=false] Optional with [raw] type [image] format. + * Boolean sets entire layer, string sets mask image, Array sets array of rectangles in format [x1,y1,x2,y2]. + * @param {string} [data.options.xmlTag] Required with [xml] flavor. Tag name containing base64 formatted data. + * @param {number} [data.options.pageWidth] Optional with [html | pdf] formats. Width of the rendering. + * Defaults to paper width. + * @param {number} [data.options.pageHeight] Optional with [html | pdf] formats. Height of the rendering. + * Defaults to paper height for [pdf], or auto sized for [html]. + * @param {string} [data.options.pageRanges] Optional with [pdf] formats. Comma-separated list of page ranges to include. + * @param {boolean} [data.options.ignoreTransparency=false] Optional with [pdf] formats. Instructs transparent PDF elements to be ignored. + * Transparent PDF elements are known to degrade performance and quality when printing. + * @param {boolean} [data.options.altFontRendering=false] Optional with [pdf] formats. Instructs PDF to be rendered using PDFBOX 1.8 techniques. + * Drastically improves low-DPI PDF print quality on Windows. + * @param {...*} [arguments] Additionally three more parameters can be specified:

+ * {boolean} [resumeOnError=false] Whether the chain should continue printing if it hits an error on one the the prints.

+ * {string|Array} [signature] Pre-signed signature(s) of the JSON string for containing call, params, and timestamp.

+ * {number|Array} [signingTimestamps] Required to match with signature. Timestamps for each of the passed pre-signed content. + * + * @returns {Promise} + * + * @see qz.configs.create + * + * @memberof qz + */ + print: function(configs, data) { + var resumeOnError = false, + signatures = [], + signaturesTimestamps = []; + + //find optional parameters + if (arguments.length >= 3) { + if (typeof arguments[2] === 'boolean') { + resumeOnError = arguments[2]; + + if (arguments.length >= 5) { + signatures = arguments[3]; + signaturesTimestamps = arguments[4]; + } + } else if (arguments.length >= 4) { + signatures = arguments[2]; + signaturesTimestamps = arguments[3]; + } + + //ensure values are arrays for consistency + if (signatures && !Array.isArray(signatures)) { signatures = [signatures]; } + if (signaturesTimestamps && !Array.isArray(signaturesTimestamps)) { signaturesTimestamps = [signaturesTimestamps]; } + } + + if (!Array.isArray(configs)) { configs = [configs]; } //single config -> array of configs + if (!Array.isArray(data[0])) { data = [data]; } //single data array -> array of data arrays + + //clean up data formatting + for(var d = 0; d < data.length; d++) { + _qz.tools.relative(data[d]); + _qz.compatible.data(data[d]); + } + + var sendToPrint = function(mapping) { + var params = { + printer: mapping.config.getPrinter(), + options: mapping.config.getOptions(), + data: mapping.data + }; + + return _qz.websocket.dataPromise('print', params, mapping.signature, mapping.timestamp); + }; + + //chain instead of Promise.all, so resumeOnError can collect each error + var chain = []; + for(var i = 0; i < configs.length || i < data.length; i++) { + (function(i_) { + var map = { + config: configs[Math.min(i_, configs.length - 1)], + data: data[Math.min(i_, data.length - 1)], + signature: signatures[i_], + timestamp: signaturesTimestamps[i_] + }; + + chain.push(function() { return sendToPrint(map) }); + })(i); + } + + //setup to catch errors if needed + var fallThrough = null; + if (resumeOnError) { + var fallen = []; + fallThrough = function(err) { fallen.push(err); }; + + //final promise to reject any errors as a group + chain.push(function() { + return _qz.tools.promise(function(resolve, reject) { + fallen.length ? reject(fallen) : resolve(); + }); + }); + } + + var last = null; + chain.reduce(function(sequence, link) { + last = sequence.catch(fallThrough).then(link); //catch is ignored if fallThrough is null + return last; + }, _qz.tools.promise(function(r) { r(); })); //an immediately resolved promise to start off the chain + + //return last promise so users can chain off final action or catch when stopping on error + return last; + }, + + + /** + * Calls related to interaction with serial ports. + * @namespace qz.serial + */ + serial: { + /** + * @returns {Promise|Error>} Communication (RS232, COM, TTY) ports available on connected system. + * + * @memberof qz.serial + */ + findPorts: function() { + return _qz.websocket.dataPromise('serial.findPorts'); + }, + + /** + * List of functions called for any response from open serial ports. + * Event data will contain {string} portName for all types. + * For RECEIVE types, {string} output. + * For ERROR types, {string} exception. + * + * @param {Function|Array} calls Single or array of Function({object} streamEvent) calls. + * + * @memberof qz.serial + */ + setSerialCallbacks: function(calls) { + _qz.serial.serialCallbacks = calls; + }, + + /** + * Opens a serial port for sending and receiving data + * + * @param {string} port Name of serial port to open. + * @param {Object} [options] Serial port configurations. + * @param {number} [options.baudRate=9600] Serial port speed. Set to 0 for auto negotiation. + * @param {number} [options.dataBits=8] Serial port data bits. Set to 0 for auto negotiation. + * @param {number} [options.stopBits=1] Serial port stop bits. Set to 0 for auto negotiation. + * @param {string} [options.parity='NONE'] Serial port parity. Set to AUTO for auto negotiation. Valid values [NONE | EVEN | ODD | MARK | SPACE | AUTO] + * @param {string} [options.flowControl='NONE'] Serial port flow control. Set to AUTO for auto negotiation. Valid values [NONE | XONXOFF | XONXOFF_OUT | XONXOFF_IN | RTSCTS | RTSCTS_OUT | RTSCTS_IN | AUTO] + * @param {string} [options.encoding='UTF-8'] Character set for communications. + * @param {string} [options.start=0x0002] DEPRECATED: Legacy character denoting start of serial response. Use options.rx.start instead. + * @param {string} [options.end=0x000D] DEPRECATED: Legacy character denoting end of serial response. Use options.rx.end instead. + * @param {number} [options.width] DEPRECATED: Legacy use for fixed-width response serial communication. Use options.rx.width instead. + * @param {Object} [options.rx] Serial communications response definitions. If an object is passed but no options are defined, all response data will be sent back as it is received unprocessed. + * @param {string|Array} [options.rx.start] Character(s) denoting start of response bytes. Used in conjunction with `end`, `width`, or `lengthbit` property. + * @param {string} [options.rx.end] Character denoting end of response bytes. Used in conjunction with `start` property. + * @param {number} [options.rx.width] Fixed width size of response bytes (not including header if `start` is set). Used alone or in conjunction with `start` property. + * @param {boolean} [options.rx.untilNewline] Returns data between newline characters (`\n` or `\r`) Truncates empty responses. Overrides `start`, `end`, `width`. + * @param {number|Object} [options.rx.lengthBytes] If a number is passed it is treated as the length index. Other values are left as their defaults. + * @param {number} [options.rx.lengthBytes.index=0] Position of the response byte (not including response `start` bytes) used to denote the length of the remaining response data. + * @param {number} [options.rx.lengthBytes.length=1] Length of response length bytes after response header. + * @param {string} [options.rx.lengthBytes.endian='BIG'] Byte endian for multi-byte length values. Valid values [BIG | LITTLE] + * @param {number|Object} [options.rx.crcBytes] If a number is passed it is treated as the crc length. Other values are left as their defaults. + * @param {number} [options.rx.crcBytes.index=0] Position after the response data (not including length or data bytes) used to denote the crc. + * @param {number} [options.rx.crcBytes.length=1] Length of response crc bytes after the response data length. + * @param {boolean} [options.rx.includeHeader=false] Whether any of the header bytes (`start` bytes and any length bytes) should be included in the processed response. + * @param {string} [options.rx.encoding] Override the encoding used for response data. Uses the same value as options.encoding otherwise. + * + * @returns {Promise} + * + * @memberof qz.serial + */ + openPort: function(port, options) { + var params = { + port: port, + options: options + }; + return _qz.websocket.dataPromise('serial.openPort', params); + }, + + /** + * Send commands over a serial port. + * Any responses from the device will be sent to serial callback functions. + * + * @param {string} port An open serial port to send data. + * @param {string|Array|Object} data Data to be sent to the serial device. + * @param {string} [data.type='PLAIN'] Valid values [FILE | PLAIN | HEX | BASE64] + * @param {string|Array} data.data Data to be sent to the serial device. + * @param {Object} options Serial port configuration updates. See qz.serial.openPort `options` docs for available values. + * For best performance, it is recommended to only set these values on the port open call. + * + * @returns {Promise} + * + * @see qz.serial.setSerialCallbacks + * + * @memberof qz.serial + */ + sendData: function(port, data, options) { + if (_qz.tools.versionCompare(2, 1, 0, 12) >= 0) { + if (typeof data !== 'object') { + data = { + data: data, + type: "PLAIN" + } + } + + if (data.type && data.type.toUpperCase() == "FILE") { + data.data = _qz.tools.absolute(data.data); + } + } + + var params = { + port: port, + data: data, + options: options + }; + return _qz.websocket.dataPromise('serial.sendData', params); + }, + + /** + * @param {string} port Name of port to close. + * + * @returns {Promise} + * + * @memberof qz.serial + */ + closePort: function(port) { + return _qz.websocket.dataPromise('serial.closePort', { port: port }); + } + }, + + /** + * Calls related to interaction with communication sockets. + * @namespace qz.socket + */ + socket: { + /** + * Opens a network port for sending and receiving data. + * + * @param {string} host The connection hostname. + * @param {number} port The connection port number. + * @param {Object} [options] Network socket configuration. + * @param {string} [options.encoding='UTF-8'] Character set for communications. + * + * @memberof qz.socket + */ + open: function(host, port, options) { + var params = { + host: host, + port: port, + options: options + }; + return _qz.websocket.dataPromise("socket.open", params); + }, + + /** + * @param {string} host The connection hostname. + * @param {number} port The connection port number. + * + * @memberof qz.socket + */ + close: function(host, port) { + var params = { + host: host, + port: port + }; + return _qz.websocket.dataPromise("socket.close", params); + }, + + /** + * Send data over an open socket. + * + * @param {string} host The connection hostname. + * @param {number} port The connection port number. + * @param {string|Object} data Data to be sent over the port. + * @param {string} [data.type='PLAIN'] Valid values [PLAIN] + * @param {string} data.data Data to be sent over the port. + * + * @memberof qz.socket + */ + sendData: function(host, port, data) { + if (typeof data !== 'object') { + data = { + data: data, + type: "PLAIN" + }; + } + + var params = { + host: host, + port: port, + data: data + }; + return _qz.websocket.dataPromise("socket.sendData", params); + }, + + /** + * List of functions called for any response from open network sockets. + * Event data will contain {string} host and {number} port for all types. + * For RECEIVE types, {string} response. + * For ERROR types, {string} exception. + * + * @param {Function|Array} calls Single or array of Function({Object} eventData) calls. + * + * @memberof qz.socket + */ + setSocketCallbacks: function(calls) { + _qz.socket.socketCallbacks = calls; + } + }, + + /** + * Calls related to interaction with USB devices. + * @namespace qz.usb + */ + usb: { + /** + * List of available USB devices. Includes (hexadecimal) vendor ID, (hexadecimal) product ID, and hub status. + * If supported, also returns manufacturer and product descriptions. + * + * @param includeHubs Whether to include USB hubs. + * @returns {Promise|Error>} Array of JSON objects containing information on connected USB devices. + * + * @memberof qz.usb + */ + listDevices: function(includeHubs) { + return _qz.websocket.dataPromise('usb.listDevices', { includeHubs: includeHubs }); + }, + + /** + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @returns {Promise|Error>} List of available (hexadecimal) interfaces on a USB device. + * + * @memberof qz.usb + */ + listInterfaces: function(deviceInfo) { + if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility + + return _qz.websocket.dataPromise('usb.listInterfaces', deviceInfo); + }, + + /** + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @param deviceInfo.iface Hex string of interface on the USB device to search. + * @returns {Promise|Error>} List of available (hexadecimal) endpoints on a USB device's interface. + * + * @memberof qz.usb + */ + listEndpoints: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + interface: arguments[2] + }; + } + + return _qz.websocket.dataPromise('usb.listEndpoints', deviceInfo); + }, + + /** + * List of functions called for any response from open usb devices. + * Event data will contain {string} vendorId and {string} productId for all types. + * For RECEIVE types, {Array} output (in hexadecimal format). + * For ERROR types, {string} exception. + * + * @param {Function|Array} calls Single or array of Function({Object} eventData) calls. + * + * @memberof qz.usb + */ + setUsbCallbacks: function(calls) { + _qz.usb.usbCallbacks = calls; + }, + + /** + * Claim a USB device's interface to enable sending/reading data across an endpoint. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @param deviceInfo.interface Hex string of interface on the USB device to claim. + * @returns {Promise} + * + * @memberof qz.usb + */ + claimDevice: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + interface: arguments[2] + }; + } + + return _qz.websocket.dataPromise('usb.claimDevice', deviceInfo); + }, + + /** + * Check the current claim state of a USB device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @returns {Promise} + * + * @since 2.0.2 + * @memberOf qz.usb + */ + isClaimed: function(deviceInfo) { + if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility + + return _qz.websocket.dataPromise('usb.isClaimed', deviceInfo); + }, + + /** + * Send data to a claimed USB device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @param deviceInfo.endpoint Hex string of endpoint on the claimed interface for the USB device. + * @param deviceInfo.data Bytes to send over specified endpoint. + * @param {string} [deviceInfo.type='PLAIN'] Valid values [FILE | PLAIN | HEX | BASE64] + * @returns {Promise} + * + * @memberof qz.usb + */ + sendData: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + endpoint: arguments[2], + data: arguments[3] + }; + } + + if (_qz.tools.versionCompare(2, 1, 0, 12) >= 0) { + if (typeof deviceInfo.data !== 'object') { + deviceInfo.data = { + data: deviceInfo.data, + type: "PLAIN" + } + } + + if (deviceInfo.data.type && deviceInfo.data.type.toUpperCase() == "FILE") { + deviceInfo.data.data = _qz.tools.absolute(deviceInfo.data.data); + } + } + + return _qz.websocket.dataPromise('usb.sendData', deviceInfo); + }, + + /** + * Read data from a claimed USB device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @param deviceInfo.endpoint Hex string of endpoint on the claimed interface for the USB device. + * @param deviceInfo.responseSize Size of the byte array to receive a response in. + * @returns {Promise|Error>} List of (hexadecimal) bytes received from the USB device. + * + * @memberof qz.usb + */ + readData: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + endpoint: arguments[2], + responseSize: arguments[3] + }; + } + + return _qz.websocket.dataPromise('usb.readData', deviceInfo); + }, + + /** + * Provides a continuous stream of read data from a claimed USB device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @param deviceInfo.endpoint Hex string of endpoint on the claimed interface for the USB device. + * @param deviceInfo.responseSize Size of the byte array to receive a response in. + * @param deviceInfo.interval=100 Frequency to send read data back, in milliseconds. + * @returns {Promise} + * + * @see qz.usb.setUsbCallbacks + * + * @memberof qz.usb + */ + openStream: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + endpoint: arguments[2], + responseSize: arguments[3], + interval: arguments[4] + }; + } + + return _qz.websocket.dataPromise('usb.openStream', deviceInfo); + }, + + /** + * Stops the stream of read data from a claimed USB device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @param deviceInfo.endpoint Hex string of endpoint on the claimed interface for the USB device. + * @returns {Promise} + * + * @memberof qz.usb + */ + closeStream: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + endpoint: arguments[2] + }; + } + + return _qz.websocket.dataPromise('usb.closeStream', deviceInfo); + }, + + /** + * Release a claimed USB device to free resources after sending/reading data. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of USB device's vendor ID. + * @param deviceInfo.productId Hex string of USB device's product ID. + * @returns {Promise} + * + * @memberof qz.usb + */ + releaseDevice: function(deviceInfo) { + if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility + + return _qz.websocket.dataPromise('usb.releaseDevice', deviceInfo); + } + }, + + + /** + * Calls related to interaction with HID USB devices
+ * Many of these calls can be accomplished from the qz.usb namespace, + * but HID allows for simpler interaction + * @namespace qz.hid + * @since 2.0.1 + */ + hid: { + /** + * List of available HID devices. Includes (hexadecimal) vendor ID and (hexadecimal) product ID. + * If available, also returns manufacturer and product descriptions. + * + * @returns {Promise|Error>} Array of JSON objects containing information on connected HID devices. + * @since 2.0.1 + * + * @memberof qz.hid + */ + listDevices: function() { + return _qz.websocket.dataPromise('hid.listDevices'); + }, + + /** + * Start listening for HID device actions, such as attach / detach events. + * Reported under the ACTION type in the streamEvent on callbacks. + * + * @returns {Promise} + * @since 2.0.1 + * + * @see qz.hid.setHidCallbacks + * + * @memberof qz.hid + */ + startListening: function() { + return _qz.websocket.dataPromise('hid.startListening'); + }, + + /** + * Stop listening for HID device actions. + * + * @returns {Promise} + * @since 2.0.1 + * + * @see qz.hid.setHidCallbacks + * + * @memberof qz.hid + */ + stopListening: function() { + return _qz.websocket.dataPromise('hid.stopListening'); + }, + + /** + * List of functions called for any response from open usb devices. + * Event data will contain {string} vendorId and {string} productId for all types. + * For RECEIVE types, {Array} output (in hexadecimal format). + * For ERROR types, {string} exception. + * For ACTION types, {string} actionType. + * + * @param {Function|Array} calls Single or array of Function({Object} eventData) calls. + * @since 2.0.1 + * + * @memberof qz.hid + */ + setHidCallbacks: function(calls) { + _qz.hid.hidCallbacks = calls; + }, + + /** + * Claim a HID device to enable sending/reading data across. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @returns {Promise} + * @since 2.0.1 + * + * @memberof qz.hid + */ + claimDevice: function(deviceInfo) { + if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility + + return _qz.websocket.dataPromise('hid.claimDevice', deviceInfo); + }, + + /** + * Check the current claim state of a HID device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @returns {Promise} + * + * @since 2.0.2 + * @memberOf qz.hid + */ + isClaimed: function(deviceInfo) { + if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility + + return _qz.websocket.dataPromise('hid.isClaimed', deviceInfo); + }, + + /** + * Send data to a claimed HID device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @param deviceInfo.data Bytes to send over specified endpoint. + * @param deviceInfo.endpoint=0x00 First byte of the data packet signifying the HID report ID. + * Must be 0x00 for devices only supporting a single report. + * @param deviceInfo.reportId=0x00 Alias for deviceInfo.endpoint. Not used if endpoint is provided. + * @param {string} [deviceInfo.type='PLAIN'] Valid values [FILE | PLAIN | HEX | BASE64] + * @returns {Promise} + * @since 2.0.1 + * + * @memberof qz.hid + */ + sendData: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + data: arguments[2], + endpoint: arguments[3] + }; + } + + if (_qz.tools.versionCompare(2, 1, 0, 12) >= 0) { + if (typeof deviceInfo.data !== 'object') { + deviceInfo.data = { + data: deviceInfo.data, + type: "PLAIN" + } + } + + if (deviceInfo.data.type && deviceInfo.data.type.toUpperCase() == "FILE") { + deviceInfo.data.data = _qz.tools.absolute(deviceInfo.data.data); + } + } else { + if (typeof deviceInfo.data === 'object') { + if (deviceInfo.data.type.toUpperCase() !== "PLAIN" + || typeof deviceInfo.data.data !== "string") { + return _qz.tools.reject(new Error("Data format is not supported with connected QZ Tray version " + _qz.websocket.connection.version)); + } + + deviceInfo.data = deviceInfo.data.data; + } + } + + return _qz.websocket.dataPromise('hid.sendData', deviceInfo); + }, + + /** + * Read data from a claimed HID device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @param deviceInfo.responseSize Size of the byte array to receive a response in. + * @returns {Promise|Error>} List of (hexadecimal) bytes received from the HID device. + * @since 2.0.1 + * + * @memberof qz.hid + */ + readData: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + responseSize: arguments[2] + }; + } + + return _qz.websocket.dataPromise('hid.readData', deviceInfo); + }, + + /** + * Send a feature report to a claimed HID device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @param deviceInfo.data Bytes to send over specified endpoint. + * @param deviceInfo.endpoint=0x00 First byte of the data packet signifying the HID report ID. + * Must be 0x00 for devices only supporting a single report. + * @param deviceInfo.reportId=0x00 Alias for deviceInfo.endpoint. Not used if endpoint is provided. + * @param {string} [deviceInfo.type='PLAIN'] Valid values [FILE | PLAIN | HEX | BASE64] + * @returns {Promise} + * + * @memberof qz.hid + */ + sendFeatureReport: function(deviceInfo) { + return _qz.websocket.dataPromise('hid.sendFeatureReport', deviceInfo); + }, + + /** + * Get a feature report from a claimed HID device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @param deviceInfo.responseSize Size of the byte array to receive a response in. + * @returns {Promise|Error>} List of (hexadecimal) bytes received from the HID device. + * + * @memberof qz.hid + */ + getFeatureReport: function(deviceInfo) { + return _qz.websocket.dataPromise('hid.getFeatureReport', deviceInfo); + }, + + /** + * Provides a continuous stream of read data from a claimed HID device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @param deviceInfo.responseSize Size of the byte array to receive a response in. + * @param deviceInfo.interval=100 Frequency to send read data back, in milliseconds. + * @returns {Promise} + * @since 2.0.1 + * + * @see qz.hid.setHidCallbacks + * + * @memberof qz.hid + */ + openStream: function(deviceInfo) { + //backwards compatibility + if (typeof deviceInfo !== 'object') { + deviceInfo = { + vendorId: arguments[0], + productId: arguments[1], + responseSize: arguments[2], + interval: arguments[3] + }; + } + + return _qz.websocket.dataPromise('hid.openStream', deviceInfo); + }, + + /** + * Stops the stream of read data from a claimed HID device. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @returns {Promise} + * @since 2.0.1 + * + * @memberof qz.hid + */ + closeStream: function(deviceInfo) { + if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility + + return _qz.websocket.dataPromise('hid.closeStream', deviceInfo); + }, + + /** + * Release a claimed HID device to free resources after sending/reading data. + * + * @param {object} deviceInfo Config details of the HID device. + * @param deviceInfo.vendorId Hex string of HID device's vendor ID. + * @param deviceInfo.productId Hex string of HID device's product ID. + * @param deviceInfo.usagePage Hex string of HID device's usage page when multiple are present. + * @param deviceInfo.serial Serial ID of HID device. + * @returns {Promise} + * @since 2.0.1 + * + * @memberof qz.hid + */ + releaseDevice: function(deviceInfo) { + if (typeof deviceInfo !== 'object') { deviceInfo = { vendorId: arguments[0], productId: arguments[1] }; } //backwards compatibility + + return _qz.websocket.dataPromise('hid.releaseDevice', deviceInfo); + } + }, + + + /** + * Calls related to interactions with the filesystem + * @namespace qz.file + * @since 2.1 + */ + file: { + /** + * List of files available at the given directory.
+ * Due to security reasons, paths are limited to the qz data directory unless overridden via properties file. + * + * @param {string} path Relative or absolute directory path. Must reside in qz data directory or a white-listed location. + * @param {Object} [params] Object containing file access parameters + * @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections + * @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user + * @returns {Promise|Error>} Array of files at the given path + * + * @memberof qz.file + */ + list: function(path, params) { + var param = _qz.tools.extend({ path: path }, params); + return _qz.websocket.dataPromise('file.list', param); + }, + + /** + * Reads contents of file at the given path.
+ * Due to security reasons, paths are limited to the qz data directory unless overridden via properties file. + * + * @param {string} path Relative or absolute file path. Must reside in qz data directory or a white-listed location. + * @param {Object} [params] Object containing file access parameters + * @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections + * @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user + * @param {string} [params.flavor='plain'] Flavor of data format used, valid flavors are [base64 | hex | plain]. + * @returns {Promise} String containing the file contents + * + * @memberof qz.file + */ + read: function(path, params) { + var param = _qz.tools.extend({ path: path }, params); + return _qz.websocket.dataPromise('file.read', param); + }, + + /** + * Writes data to the file at the given path.
+ * Due to security reasons, paths are limited to the qz data directory unless overridden via properties file. + * + * @param {string} path Relative or absolute file path. Must reside in qz data directory or a white-listed location. + * @param {Object} params Object containing file access parameters + * @param {string} params.data File data to be written + * @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections + * @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user + * @param {boolean} [params.append=false] Appends to the end of the file if set, otherwise overwrites existing contents + * @param {string} [params.flavor='plain'] Flavor of data format used, valid flavors are [base64 | file | hex | plain]. + * @returns {Promise} + * + * @memberof qz.file + */ + write: function(path, params) { + var param = _qz.tools.extend({ path: path }, params); + return _qz.websocket.dataPromise('file.write', param); + }, + + /** + * Deletes a file at given path.
+ * Due to security reasons, paths are limited to the qz data directory unless overridden via properties file. + * + * @param {string} path Relative or absolute file path. Must reside in qz data directory or a white-listed location. + * @param {Object} [params] Object containing file access parameters + * @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections + * @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user + * @returns {Promise} + * + * @memberof qz.file + */ + remove: function(path, params) { + var param = _qz.tools.extend({ path: path }, params); + return _qz.websocket.dataPromise('file.remove', param); + }, + + /** + * Provides a continuous stream of events (and optionally data) from a local file. + * + * @param {string} path Relative or absolute directory path. Must reside in qz data directory or a white-listed location. + * @param {Object} [params] Object containing file access parameters + * @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections + * @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user + * @param {Object} [params.listener] If defined, file data will be returned on events + * @param {number} [params.listener.bytes=-1] Number of bytes to return or -1 for all + * @param {number} [params.listener.lines=-1] Number of lines to return or -1 for all + * @param {boolean} [params.listener.reverse] Controls whether data should be returned from the bottom of the file. Default value is true for line mode and false for byte mode. + * @param {string|Array} [params.include] File patterns to match. Blank values will be ignored. + * @param {string|Array} [params.exclude] File patterns to exclude. Blank values will be ignored. Takes priority over params.include. + * @param {boolean} [params.ignoreCase=true] Whether params.include or params.exclude are case-sensitive. + * @returns {Promise} + * @since 2.1.0 + * + * @see qz.file.setFileCallbacks + * + * @memberof qz.file + */ + startListening: function(path, params) { + if (params && typeof params.include !== 'undefined' && !Array.isArray(params.include)) { + params.include = [params.include]; + } + if (params && typeof params.exclude !== 'undefined' && !Array.isArray(params.exclude)) { + params.exclude = [params.exclude]; + } + var param = _qz.tools.extend({ path: path }, params); + return _qz.websocket.dataPromise('file.startListening', param); + }, + + /** + * Closes listeners with the provided settings. Omitting the path parameter will result in all listeners closing. + * + * @param {string} [path] Previously opened directory path of listener to close, or omit to close all. + * @param {Object} [params] Object containing file access parameters + * @param {boolean} [params.sandbox=true] If relative location from root is only available to the certificate's connection, otherwise all connections + * @param {boolean} [params.shared=true] If relative location from root is accessible to all users on the system, otherwise just the current user + * @returns {Promise} + * + * @memberof qz.file + */ + stopListening: function(path, params) { + var param = _qz.tools.extend({ path: path }, params); + return _qz.websocket.dataPromise('file.stopListening', param); + }, + + /** + * List of functions called for any response from a file listener. + * For ERROR types event data will contain, {string} message. + * For ACTION types event data will contain, {string} file {string} eventType {string} [data]. + * + * @param {Function|Array} calls Single or array of Function({Object} eventData) calls. + * @since 2.1.0 + * + * @memberof qz.file + */ + setFileCallbacks: function(calls) { + _qz.file.fileCallbacks = calls; + } + }, + + /** + * Calls related to networking information + * @namespace qz.networking + * @since 2.1.0 + */ + networking: { + /** + * @param {string} [hostname] Hostname to try to connect to when determining network interfaces, defaults to "google.com" + * @param {number} [port] Port to use with custom hostname, defaults to 443 + * @returns {Promise} Connected system's network information. + * + * @memberof qz.networking + * @since 2.1.0 + */ + device: function(hostname, port) { + // Wrap 2.0 + if (_qz.tools.isVersion(2, 0)) { + return _qz.compatible.networking(hostname, port, null, null, function(data) { + return { ip: data.ipAddress, mac: data.macAddress }; + }); + } + // Use 2.1 + return _qz.websocket.dataPromise('networking.device', { + hostname: hostname, + port: port + }); + }, + + /** + * Get computer hostname + * + * @param {string} [hostname] DEPRECATED Hostname to try to connect to when determining network interfaces, defaults to "google.com" + * @param {number} [port] DEPRECATED Port to use with custom hostname, defaults to 443 + * @returns {Promise} Connected system's hostname. + * + * @memberof qz.networking + * @since 2.2.2 + */ + hostname: function(hostname, port) { + // Wrap < 2.2.2 + if (_qz.tools.versionCompare(2, 2, 2) < 0) { + return _qz.tools.promise(function(resolve, reject) { + _qz.websocket.dataPromise('networking.device', { hostname: hostname, port: port }).then(function(device) { + console.log(device); + resolve(device.hostname); + }); + }); + } else { + return _qz.websocket.dataPromise('networking.hostname'); + } + }, + + /** + * @param {string} [hostname] Hostname to try to connect to when determining network interfaces, defaults to "google.com" + * @param {number} [port] Port to use with custom hostname, defaults to 443 + * @returns {Promise|Error>} Connected system's network information. + * + * @memberof qz.networking + * @since 2.1.0 + */ + devices: function(hostname, port) { + // Wrap 2.0 + if (_qz.tools.isVersion(2, 0)) { + return _qz.compatible.networking(hostname, port, null, null, function(data) { + return [{ ip: data.ipAddress, mac: data.macAddress }]; + }); + } + // Use 2.1 + return _qz.websocket.dataPromise('networking.devices', { + hostname: hostname, + port: port + }); + } + }, + + + /** + * Calls related to signing connection requests. + * @namespace qz.security + */ + security: { + /** + * Set promise resolver for calls to acquire the site's certificate. + * + * @param {Function|AsyncFunction|Promise} promiseHandler Either a function that will be used as a promise resolver (of format Function({function} resolve, {function}reject)), + * an async function, or a promise. Any of which should return the public certificate via their respective resolve call. + * @param {Object} [options] Configuration options for the certificate resolver + * @param {boolean} [options.rejectOnFailure=[false]] Overrides default behavior to call resolve with a blank certificate on failure. + * @memberof qz.security + */ + setCertificatePromise: function(promiseHandler, options) { + _qz.security.certHandler = promiseHandler; + _qz.security.rejectOnCertFailure = !!(options && options.rejectOnFailure); + }, + + /** + * Set promise factory for calls to sign API calls. + * + * @param {Function|AsyncFunction} promiseFactory Either a function that accepts a string parameter of the data to be signed + * and returns a function to be used as a promise resolver (of format Function({function} resolve, {function}reject)), + * or an async function that can take a string parameter of the data to be signed. Either of which should return the signed contents of + * the passed string parameter via their respective resolve call. + * + * @example + * qz.security.setSignaturePromise(function(dataToSign) { + * return function(resolve, reject) { + * $.ajax("/signing-url?data=" + dataToSign).then(resolve, reject); + * } + * }) + * + * @memberof qz.security + */ + setSignaturePromise: function(promiseFactory) { + _qz.security.signatureFactory = promiseFactory; + }, + + /** + * Set which signing algorithm QZ will check signatures against. + * + * @param {string} algorithm The algorithm used in signing. Valid values: [SHA1 | SHA256 | SHA512] + * @since 2.1.0 + * + * @memberof qz.security + */ + setSignatureAlgorithm: function(algorithm) { + //warn for incompatibilities if known + if (!_qz.compatible.algorithm()) { + return; + } + + if (["SHA1", "SHA256", "SHA512"].indexOf(algorithm.toUpperCase()) < 0) { + _qz.log.error("Signing algorithm '" + algorithm + "' is not supported."); + } else { + _qz.security.signAlgorithm = algorithm; + } + }, + + /** + * Get the signing algorithm QZ will be checking signatures against. + * + * @returns {string} The algorithm used in signing. + * @since 2.1.0 + * + * @memberof qz.security + */ + getSignatureAlgorithm: function() { + return _qz.security.signAlgorithm; + } + }, + + /** + * Calls related to compatibility adjustments + * @namespace qz.api + */ + api: { + /** + * Show or hide QZ api debugging statements in the browser console. + * + * @param {boolean} show Whether the debugging logs for QZ should be shown. Hidden by default. + * @returns {boolean} Value of debugging flag + * @memberof qz.api + */ + showDebug: function(show) { + return (_qz.DEBUG = show); + }, + + /** + * Get version of connected QZ Tray application. + * + * @returns {Promise} Version number of QZ Tray. + * + * @memberof qz.api + */ + getVersion: function() { + return _qz.websocket.dataPromise('getVersion'); + }, + + /** + * Checks for the specified version of connected QZ Tray application. + * + * @param {string|number} [major] Major version to check + * @param {string|number} [minor] Minor version to check + * @param {string|number} [patch] Patch version to check + * + * @memberof qz.api + */ + isVersion: _qz.tools.isVersion, + + /** + * Checks if the connected QZ Tray application is greater than the specified version. + * + * @param {string|number} major Major version to check + * @param {string|number} [minor] Minor version to check + * @param {string|number} [patch] Patch version to check + * @param {string|number} [build] Build version to check + * @returns {boolean} True if connected version is greater than the version specified. + * + * @memberof qz.api + * @since 2.1.0-4 + */ + isVersionGreater: function(major, minor, patch, build) { + return _qz.tools.versionCompare(major, minor, patch, build) > 0; + }, + + /** + * Checks if the connected QZ Tray application is less than the specified version. + * + * @param {string|number} major Major version to check + * @param {string|number} [minor] Minor version to check + * @param {string|number} [patch] Patch version to check + * @param {string|number} [build] Build version to check + * @returns {boolean} True if connected version is less than the version specified. + * + * @memberof qz.api + * @since 2.1.0-4 + */ + isVersionLess: function(major, minor, patch, build) { + return _qz.tools.versionCompare(major, minor, patch, build) < 0; + }, + + /** + * Change the promise library used by QZ API. + * Should be called before any initialization to avoid possible errors. + * + * @param {Function} promiser Function({function} resolver) called to create new promises. + * + * @memberof qz.api + */ + setPromiseType: function(promiser) { + _qz.tools.promise = promiser; + }, + + /** + * Change the SHA-256 hashing function used by QZ API. + * Should be called before any initialization to avoid possible errors. + * + * @param {Function} hasher Function({function} message) called to create hash of passed string. + * + * @memberof qz.api + */ + setSha256Type: function(hasher) { + _qz.tools.hash = hasher; + }, + + /** + * Change the WebSocket handler. + * Should be called before any initialization to avoid possible errors. + * + * @param {Function} ws Function({function} WebSocket) called to override the internal WebSocket handler. + * + * @memberof qz.api + */ + setWebSocketType: function(ws) { + _qz.tools.ws = ws; + } + }, + + /** + * Version of this JavaScript library + * + * @constant {string} + * + * @memberof qz + */ + version: _qz.VERSION + }; + + return qz; +})(); + + +(function() { + if (typeof define === 'function' && define.amd) { + define(qz); + } else if (typeof exports === 'object') { + module.exports = qz; + } else { + window.qz = qz; + } +})(); diff --git a/base_report_to_qz_tray/views/ir_actions_report.xml b/base_report_to_qz_tray/views/ir_actions_report.xml new file mode 100644 index 00000000000..8d89aaf1610 --- /dev/null +++ b/base_report_to_qz_tray/views/ir_actions_report.xml @@ -0,0 +1,22 @@ + + + + ir.actions.report.form (in base_report_to_printer) + ir.actions.report + + + + + + + + + + + + + + + + + diff --git a/base_report_to_qz_tray/views/res_users.xml b/base_report_to_qz_tray/views/res_users.xml new file mode 100644 index 00000000000..f28e8c0b844 --- /dev/null +++ b/base_report_to_qz_tray/views/res_users.xml @@ -0,0 +1,29 @@ + + + + res.users.form (in base_report_to_qz_tray) + res.users + + + + + + + + + + + + res.users.form.simple (in base_report_to_qz_tray) + res.users + + + + + + + + + + + diff --git a/requirements.txt b/requirements.txt index c9e7b42441c..f832bdaab87 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ # generated from manifests external_dependencies oauthlib pycups +pyOpenSSL requests_oauthlib