odoo-addon-base-report-to-printer 18.0.1.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odoo/addons/base_report_to_printer/README.rst +205 -0
- odoo/addons/base_report_to_printer/__init__.py +9 -0
- odoo/addons/base_report_to_printer/__manifest__.py +39 -0
- odoo/addons/base_report_to_printer/data/neutralize.sql +2 -0
- odoo/addons/base_report_to_printer/data/printing_data.xml +27 -0
- odoo/addons/base_report_to_printer/i18n/am.po +936 -0
- odoo/addons/base_report_to_printer/i18n/base_report_to_printer.pot +932 -0
- odoo/addons/base_report_to_printer/i18n/bg.po +939 -0
- odoo/addons/base_report_to_printer/i18n/ca.po +936 -0
- odoo/addons/base_report_to_printer/i18n/de.po +994 -0
- odoo/addons/base_report_to_printer/i18n/el_GR.po +937 -0
- odoo/addons/base_report_to_printer/i18n/es.po +1004 -0
- odoo/addons/base_report_to_printer/i18n/es_AR.po +987 -0
- odoo/addons/base_report_to_printer/i18n/es_ES.po +937 -0
- odoo/addons/base_report_to_printer/i18n/fi.po +939 -0
- odoo/addons/base_report_to_printer/i18n/fr.po +999 -0
- odoo/addons/base_report_to_printer/i18n/gl.po +936 -0
- odoo/addons/base_report_to_printer/i18n/hr.po +966 -0
- odoo/addons/base_report_to_printer/i18n/hr_HR.po +941 -0
- odoo/addons/base_report_to_printer/i18n/it.po +985 -0
- odoo/addons/base_report_to_printer/i18n/nl.po +939 -0
- odoo/addons/base_report_to_printer/i18n/nl_NL.po +950 -0
- odoo/addons/base_report_to_printer/i18n/pt.po +936 -0
- odoo/addons/base_report_to_printer/i18n/pt_BR.po +940 -0
- odoo/addons/base_report_to_printer/i18n/pt_PT.po +937 -0
- odoo/addons/base_report_to_printer/i18n/sl.po +943 -0
- odoo/addons/base_report_to_printer/i18n/sv.po +983 -0
- odoo/addons/base_report_to_printer/i18n/tr.po +936 -0
- odoo/addons/base_report_to_printer/i18n/zh_CN.po +958 -0
- odoo/addons/base_report_to_printer/models/__init__.py +8 -0
- odoo/addons/base_report_to_printer/models/ir_actions_report.py +253 -0
- odoo/addons/base_report_to_printer/models/printing_action.py +26 -0
- odoo/addons/base_report_to_printer/models/printing_job.py +131 -0
- odoo/addons/base_report_to_printer/models/printing_printer.py +268 -0
- odoo/addons/base_report_to_printer/models/printing_report_xml_action.py +48 -0
- odoo/addons/base_report_to_printer/models/printing_server.py +275 -0
- odoo/addons/base_report_to_printer/models/printing_tray.py +21 -0
- odoo/addons/base_report_to_printer/models/res_users.py +55 -0
- odoo/addons/base_report_to_printer/readme/CONFIGURE.md +13 -0
- odoo/addons/base_report_to_printer/readme/CONTRIBUTORS.md +18 -0
- odoo/addons/base_report_to_printer/readme/CREDITS.md +1 -0
- odoo/addons/base_report_to_printer/readme/DESCRIPTION.md +27 -0
- odoo/addons/base_report_to_printer/readme/HISTORY.md +7 -0
- odoo/addons/base_report_to_printer/readme/INSTALL.md +10 -0
- odoo/addons/base_report_to_printer/readme/ROADMAP.md +3 -0
- odoo/addons/base_report_to_printer/readme/USAGE.md +15 -0
- odoo/addons/base_report_to_printer/security/ir.model.access.csv +2 -0
- odoo/addons/base_report_to_printer/security/security.xml +151 -0
- odoo/addons/base_report_to_printer/static/description/icon.png +0 -0
- odoo/addons/base_report_to_printer/static/description/index.html +561 -0
- odoo/addons/base_report_to_printer/static/src/js/qweb_action_manager.esm.js +92 -0
- odoo/addons/base_report_to_printer/tests/__init__.py +13 -0
- odoo/addons/base_report_to_printer/tests/test_ir_actions_report.py +350 -0
- odoo/addons/base_report_to_printer/tests/test_printing_job.py +70 -0
- odoo/addons/base_report_to_printer/tests/test_printing_printer.py +198 -0
- odoo/addons/base_report_to_printer/tests/test_printing_printer_tray.py +256 -0
- odoo/addons/base_report_to_printer/tests/test_printing_printer_wizard.py +94 -0
- odoo/addons/base_report_to_printer/tests/test_printing_report_xml_action.py +98 -0
- odoo/addons/base_report_to_printer/tests/test_printing_server.py +219 -0
- odoo/addons/base_report_to_printer/tests/test_printing_tray.py +49 -0
- odoo/addons/base_report_to_printer/tests/test_report.py +226 -0
- odoo/addons/base_report_to_printer/tests/test_res_users.py +53 -0
- odoo/addons/base_report_to_printer/views/ir_actions_report.xml +21 -0
- odoo/addons/base_report_to_printer/views/printing_job.xml +46 -0
- odoo/addons/base_report_to_printer/views/printing_printer.xml +147 -0
- odoo/addons/base_report_to_printer/views/printing_report.xml +39 -0
- odoo/addons/base_report_to_printer/views/printing_server.xml +79 -0
- odoo/addons/base_report_to_printer/views/res_users.xml +33 -0
- odoo/addons/base_report_to_printer/wizards/__init__.py +2 -0
- odoo/addons/base_report_to_printer/wizards/print_attachment_report.py +80 -0
- odoo/addons/base_report_to_printer/wizards/print_attachment_report.xml +56 -0
- odoo/addons/base_report_to_printer/wizards/printing_printer_update_wizard.py +27 -0
- odoo/addons/base_report_to_printer/wizards/printing_printer_update_wizard_view.xml +37 -0
- odoo_addon_base_report_to_printer-18.0.1.1.6.dist-info/METADATA +222 -0
- odoo_addon_base_report_to_printer-18.0.1.1.6.dist-info/RECORD +77 -0
- odoo_addon_base_report_to_printer-18.0.1.1.6.dist-info/WHEEL +5 -0
- odoo_addon_base_report_to_printer-18.0.1.1.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# Copyright (c) 2007 Ferran Pegueroles <ferran@pegueroles.com>
|
|
2
|
+
# Copyright (c) 2009 Albert Cervera i Areny <albert@nan-tic.com>
|
|
3
|
+
# Copyright (C) 2011 Agile Business Group sagl (<http://www.agilebg.com>)
|
|
4
|
+
# Copyright (C) 2011 Domsense srl (<http://www.domsense.com>)
|
|
5
|
+
# Copyright (C) 2013-2014 Camptocamp (<http://www.camptocamp.com>)
|
|
6
|
+
# Copyright 2024 Tecnativa - Sergio Teruel
|
|
7
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
8
|
+
import threading
|
|
9
|
+
from time import time
|
|
10
|
+
|
|
11
|
+
from odoo import _, api, exceptions, fields, models, registry
|
|
12
|
+
from odoo.tools.safe_eval import safe_eval
|
|
13
|
+
|
|
14
|
+
REPORT_TYPES = {"qweb-pdf": "pdf", "qweb-text": "text"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class IrActionsReport(models.Model):
|
|
18
|
+
_inherit = "ir.actions.report"
|
|
19
|
+
|
|
20
|
+
property_printing_action_id = fields.Many2one(
|
|
21
|
+
comodel_name="printing.action",
|
|
22
|
+
string="Default Behaviour",
|
|
23
|
+
company_dependent=True,
|
|
24
|
+
)
|
|
25
|
+
printing_printer_id = fields.Many2one(
|
|
26
|
+
comodel_name="printing.printer", string="Default Printer"
|
|
27
|
+
)
|
|
28
|
+
printer_tray_id = fields.Many2one(
|
|
29
|
+
comodel_name="printing.tray",
|
|
30
|
+
string="Paper Source",
|
|
31
|
+
domain="[('printer_id', '=', printing_printer_id)]",
|
|
32
|
+
)
|
|
33
|
+
printing_action_ids = fields.One2many(
|
|
34
|
+
comodel_name="printing.report.xml.action",
|
|
35
|
+
inverse_name="report_id",
|
|
36
|
+
string="Actions",
|
|
37
|
+
help="This field allows configuring action and printer on a per user basis",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
@api.onchange("printing_printer_id")
|
|
41
|
+
def onchange_printing_printer_id(self):
|
|
42
|
+
"""Reset the tray when the printer is changed"""
|
|
43
|
+
self.printer_tray_id = False
|
|
44
|
+
|
|
45
|
+
@api.model
|
|
46
|
+
def print_action_for_report_name(self, report_name):
|
|
47
|
+
"""Returns if the action is a direct print or pdf
|
|
48
|
+
|
|
49
|
+
Called from js
|
|
50
|
+
"""
|
|
51
|
+
report = self._get_report_from_name(report_name)
|
|
52
|
+
if not report:
|
|
53
|
+
return {}
|
|
54
|
+
result = report.behaviour()
|
|
55
|
+
serializable_result = {
|
|
56
|
+
"action": result["action"],
|
|
57
|
+
"printer_name": result["printer"].name,
|
|
58
|
+
}
|
|
59
|
+
if result.get("printer_exception") and not self.env.context.get(
|
|
60
|
+
"skip_printer_exception"
|
|
61
|
+
):
|
|
62
|
+
serializable_result["printer_exception"] = True
|
|
63
|
+
if self.env.context.get("force_print_to_client"):
|
|
64
|
+
serializable_result["action"] = "client"
|
|
65
|
+
return serializable_result
|
|
66
|
+
|
|
67
|
+
def _get_user_default_printer(self, user):
|
|
68
|
+
return user.printing_printer_id
|
|
69
|
+
|
|
70
|
+
def _get_user_default_print_behaviour(self):
|
|
71
|
+
printer_obj = self.env["printing.printer"]
|
|
72
|
+
user = self.env.user
|
|
73
|
+
printer = self._get_user_default_printer(user)
|
|
74
|
+
return dict(
|
|
75
|
+
action=user.printing_action or "client",
|
|
76
|
+
printer=printer or printer_obj.get_default(),
|
|
77
|
+
tray=(
|
|
78
|
+
str(user.printer_tray_id.system_name) if user.printer_tray_id else False
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def _get_report_default_print_behaviour(self):
|
|
83
|
+
result = {}
|
|
84
|
+
report_action = self.property_printing_action_id
|
|
85
|
+
if report_action and report_action.action_type != "user_default":
|
|
86
|
+
result["action"] = report_action.action_type
|
|
87
|
+
if self.printing_printer_id:
|
|
88
|
+
result["printer"] = self.printing_printer_id
|
|
89
|
+
if self.printer_tray_id:
|
|
90
|
+
result["tray"] = self.printer_tray_id.system_name
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
def behaviour(self):
|
|
94
|
+
self.ensure_one()
|
|
95
|
+
printing_act_obj = self.env["printing.report.xml.action"]
|
|
96
|
+
|
|
97
|
+
result = self._get_user_default_print_behaviour()
|
|
98
|
+
result.update(self._get_report_default_print_behaviour())
|
|
99
|
+
|
|
100
|
+
# Retrieve report-user specific values
|
|
101
|
+
print_action = printing_act_obj.search(
|
|
102
|
+
[
|
|
103
|
+
("report_id", "=", self.id),
|
|
104
|
+
("user_id", "=", self.env.uid),
|
|
105
|
+
("action", "!=", "user_default"),
|
|
106
|
+
],
|
|
107
|
+
limit=1,
|
|
108
|
+
)
|
|
109
|
+
if print_action:
|
|
110
|
+
# For some reason design takes report defaults over
|
|
111
|
+
# False action entries so we must allow for that here
|
|
112
|
+
result.update({k: v for k, v in print_action.behaviour().items() if v})
|
|
113
|
+
printer = result.get("printer")
|
|
114
|
+
if printer:
|
|
115
|
+
# When no printer is available we can fallback to the default behavior
|
|
116
|
+
# letting the user to manually print the reports.
|
|
117
|
+
try:
|
|
118
|
+
printer.server_id._open_connection(raise_on_error=True)
|
|
119
|
+
printer_exception = printer.status in [
|
|
120
|
+
"error",
|
|
121
|
+
"server-error",
|
|
122
|
+
"unavailable",
|
|
123
|
+
]
|
|
124
|
+
except Exception:
|
|
125
|
+
printer_exception = True
|
|
126
|
+
if printer_exception and not self.env.context.get("skip_printer_exception"):
|
|
127
|
+
result["printer_exception"] = True
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
def print_document_client_action(self, record_ids, data=None):
|
|
131
|
+
behaviour = self.behaviour()
|
|
132
|
+
printer = behaviour.pop("printer", None)
|
|
133
|
+
if printer.multi_thread:
|
|
134
|
+
|
|
135
|
+
@self.env.cr.postcommit.add
|
|
136
|
+
def _launch_print_thread():
|
|
137
|
+
threaded_calculation = threading.Thread(
|
|
138
|
+
target=self.print_document_threaded,
|
|
139
|
+
args=(self.id, record_ids, data),
|
|
140
|
+
)
|
|
141
|
+
threaded_calculation.start()
|
|
142
|
+
|
|
143
|
+
return True
|
|
144
|
+
else:
|
|
145
|
+
try:
|
|
146
|
+
return self.print_document(record_ids, data=data)
|
|
147
|
+
except Exception:
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
def print_document_threaded(self, report_id, record_ids, data):
|
|
151
|
+
with registry(self._cr.dbname).cursor() as cr:
|
|
152
|
+
self = self.with_env(self.env(cr=cr))
|
|
153
|
+
report = self.env["ir.actions.report"].browse(report_id)
|
|
154
|
+
report.print_document(record_ids, data)
|
|
155
|
+
|
|
156
|
+
def print_document(self, record_ids, data=None):
|
|
157
|
+
"""Print a document, do not return the document file"""
|
|
158
|
+
report_type = REPORT_TYPES.get(self.report_type)
|
|
159
|
+
if not report_type:
|
|
160
|
+
raise exceptions.UserError(
|
|
161
|
+
_("This report type (%s) is not supported by direct printing!")
|
|
162
|
+
% str(self.report_type)
|
|
163
|
+
)
|
|
164
|
+
method_name = f"_render_qweb_{report_type}"
|
|
165
|
+
document, doc_format = getattr(
|
|
166
|
+
self.with_context(must_skip_send_to_printer=True), method_name
|
|
167
|
+
)(self.report_name, record_ids, data=data)
|
|
168
|
+
behaviour = self.behaviour()
|
|
169
|
+
printer = behaviour.pop("printer", None)
|
|
170
|
+
|
|
171
|
+
if not printer:
|
|
172
|
+
raise exceptions.UserError(_("No printer configured to print this report."))
|
|
173
|
+
if self.print_report_name:
|
|
174
|
+
report_file_names = [
|
|
175
|
+
safe_eval(self.print_report_name, {"object": obj, "time": time})
|
|
176
|
+
for obj in self.env[self.model].browse(record_ids)
|
|
177
|
+
]
|
|
178
|
+
title = " ".join(report_file_names)
|
|
179
|
+
if len(title) > 80:
|
|
180
|
+
title = title[:80] + "…"
|
|
181
|
+
else:
|
|
182
|
+
title = self.report_name
|
|
183
|
+
behaviour["title"] = title
|
|
184
|
+
behaviour["res_ids"] = record_ids
|
|
185
|
+
# TODO should we use doc_format instead of report_type
|
|
186
|
+
return printer.print_document(
|
|
187
|
+
self, document, doc_format=self.report_type, **behaviour
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def _can_print_report(self, behaviour, printer, document):
|
|
191
|
+
"""Predicate that decide if report can be sent to printer
|
|
192
|
+
|
|
193
|
+
If you want to prevent `render_qweb_pdf` to send report you can set
|
|
194
|
+
the `must_skip_send_to_printer` key to True in the context
|
|
195
|
+
"""
|
|
196
|
+
if self.env.context.get("must_skip_send_to_printer"):
|
|
197
|
+
return False
|
|
198
|
+
if (
|
|
199
|
+
behaviour["action"] == "server"
|
|
200
|
+
and printer
|
|
201
|
+
and document
|
|
202
|
+
and not behaviour.get("printer_exception")
|
|
203
|
+
):
|
|
204
|
+
return True
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
def report_action(self, docids, data=None, config=True):
|
|
208
|
+
res = super().report_action(docids, data=data, config=config)
|
|
209
|
+
if not res.get("id"):
|
|
210
|
+
res["id"] = self.id
|
|
211
|
+
return res
|
|
212
|
+
|
|
213
|
+
def _render_qweb_pdf(self, report_ref, res_ids=None, data=None):
|
|
214
|
+
"""Generate a PDF and returns it.
|
|
215
|
+
|
|
216
|
+
If the action configured on the report is server, it prints the
|
|
217
|
+
generated document as well.
|
|
218
|
+
"""
|
|
219
|
+
document, doc_format = super()._render_qweb_pdf(
|
|
220
|
+
report_ref, res_ids=res_ids, data=data
|
|
221
|
+
)
|
|
222
|
+
report = self._get_report(report_ref)
|
|
223
|
+
behaviour = report.behaviour()
|
|
224
|
+
printer = behaviour.pop("printer", None)
|
|
225
|
+
can_print_report = report._can_print_report(behaviour, printer, document)
|
|
226
|
+
|
|
227
|
+
if can_print_report:
|
|
228
|
+
printer.print_document(
|
|
229
|
+
report, document, doc_format=report.report_type, **behaviour
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return document, doc_format
|
|
233
|
+
|
|
234
|
+
def _render_qweb_text(self, report_ref, docids, data=None):
|
|
235
|
+
"""Generate a TEXT file and returns it.
|
|
236
|
+
|
|
237
|
+
If the action configured on the report is server, it prints the
|
|
238
|
+
generated document as well.
|
|
239
|
+
"""
|
|
240
|
+
document, doc_format = super()._render_qweb_text(
|
|
241
|
+
report_ref, docids=docids, data=data
|
|
242
|
+
)
|
|
243
|
+
report = self._get_report(report_ref)
|
|
244
|
+
behaviour = report.behaviour()
|
|
245
|
+
printer = behaviour.pop("printer", None)
|
|
246
|
+
can_print_report = report._can_print_report(behaviour, printer, document)
|
|
247
|
+
|
|
248
|
+
if can_print_report:
|
|
249
|
+
printer.print_document(
|
|
250
|
+
report, document, doc_format=report.report_type, **behaviour
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return document, doc_format
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Copyright (c) 2007 Ferran Pegueroles <ferran@pegueroles.com>
|
|
2
|
+
# Copyright (c) 2009 Albert Cervera i Areny <albert@nan-tic.com>
|
|
3
|
+
# Copyright (C) 2011 Agile Business Group sagl (<http://www.agilebg.com>)
|
|
4
|
+
# Copyright (C) 2011 Domsense srl (<http://www.domsense.com>)
|
|
5
|
+
# Copyright (C) 2013-2014 Camptocamp (<http://www.camptocamp.com>)
|
|
6
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
7
|
+
|
|
8
|
+
from odoo import fields, models
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PrintingAction(models.Model):
|
|
12
|
+
_name = "printing.action"
|
|
13
|
+
_description = "Print Job Action"
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def _available_action_types(self):
|
|
17
|
+
return [
|
|
18
|
+
("server", "Send to Printer"),
|
|
19
|
+
("client", "Send to Client"),
|
|
20
|
+
("user_default", "Use user's defaults"),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
name = fields.Char(required=True)
|
|
24
|
+
action_type = fields.Selection(
|
|
25
|
+
selection=_available_action_types, string="Type", required=True
|
|
26
|
+
)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Copyright (C) 2016 SYLEAM (<http://www.syleam.fr>)
|
|
2
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from odoo import fields, models
|
|
7
|
+
|
|
8
|
+
_logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PrintingJob(models.Model):
|
|
12
|
+
_name = "printing.job"
|
|
13
|
+
_description = "Printing Job"
|
|
14
|
+
_order = "job_id_cups DESC"
|
|
15
|
+
|
|
16
|
+
name = fields.Char(help="Job name.")
|
|
17
|
+
active = fields.Boolean(
|
|
18
|
+
default=True, help="Unchecked if the job is purged from CUPS."
|
|
19
|
+
)
|
|
20
|
+
job_id_cups = fields.Integer(
|
|
21
|
+
string="Job ID", required=True, help="CUPS id for this job."
|
|
22
|
+
)
|
|
23
|
+
server_id = fields.Many2one(
|
|
24
|
+
comodel_name="printing.server",
|
|
25
|
+
string="Server",
|
|
26
|
+
related="printer_id.server_id",
|
|
27
|
+
store=True,
|
|
28
|
+
help="Server which hosts this job.",
|
|
29
|
+
)
|
|
30
|
+
printer_id = fields.Many2one(
|
|
31
|
+
comodel_name="printing.printer",
|
|
32
|
+
string="Printer",
|
|
33
|
+
required=True,
|
|
34
|
+
ondelete="cascade",
|
|
35
|
+
help="Printer used for this job.",
|
|
36
|
+
)
|
|
37
|
+
job_media_progress = fields.Integer(
|
|
38
|
+
string="Media Progress",
|
|
39
|
+
required=True,
|
|
40
|
+
help="Percentage of progress for this job.",
|
|
41
|
+
)
|
|
42
|
+
time_at_creation = fields.Datetime(
|
|
43
|
+
string="Creation Date",
|
|
44
|
+
required=True,
|
|
45
|
+
help="Date and time of creation of this job.",
|
|
46
|
+
)
|
|
47
|
+
time_at_processing = fields.Datetime(
|
|
48
|
+
string="Processing Date", help="Date and time of process for this job."
|
|
49
|
+
)
|
|
50
|
+
time_at_completed = fields.Datetime(
|
|
51
|
+
string="Completion Date", help="Date and time of completion for this job."
|
|
52
|
+
)
|
|
53
|
+
job_state = fields.Selection(
|
|
54
|
+
selection=[
|
|
55
|
+
("pending", "Pending"),
|
|
56
|
+
("pending held", "Pending Held"),
|
|
57
|
+
("processing", "Processing"),
|
|
58
|
+
("processing stopped", "Processing Stopped"),
|
|
59
|
+
("canceled", "Canceled"),
|
|
60
|
+
("aborted", "Aborted"),
|
|
61
|
+
("completed", "Completed"),
|
|
62
|
+
("unknown", "Unknown"),
|
|
63
|
+
],
|
|
64
|
+
string="State",
|
|
65
|
+
help="Current state of the job.",
|
|
66
|
+
)
|
|
67
|
+
job_state_reason = fields.Selection(
|
|
68
|
+
selection=[
|
|
69
|
+
("none", "No reason"),
|
|
70
|
+
("aborted-by-system", "Aborted by the system"),
|
|
71
|
+
("compression-error", "Error in the compressed data"),
|
|
72
|
+
("cups-filter-crashed", "CUPS filter crashed"),
|
|
73
|
+
("document-access-error", "The URI cannot be accessed"),
|
|
74
|
+
("document-format-error", "Error in the document"),
|
|
75
|
+
("job-canceled-at-device", "Cancelled at the device"),
|
|
76
|
+
("job-canceled-by-operator", "Cancelled by the printer operator"),
|
|
77
|
+
("job-canceled-by-user", "Cancelled by the user"),
|
|
78
|
+
("job-completed-successfully", "Completed successfully"),
|
|
79
|
+
("job-completed-with-errors", "Completed with some errors"),
|
|
80
|
+
("job-completed-with-warnings", "Completed with some warnings"),
|
|
81
|
+
("job-data-insufficient", "No data has been received"),
|
|
82
|
+
("job-hold-until-specified", "Currently held"),
|
|
83
|
+
("job-incoming", "Files are currently being received"),
|
|
84
|
+
("job-interpreting", "Currently being interpreted"),
|
|
85
|
+
("job-outgoing", "Currently being sent to the printer"),
|
|
86
|
+
("job-printing", "Currently printing"),
|
|
87
|
+
("job-queued", "Queued for printing"),
|
|
88
|
+
("job-queued-for-marker", "Printer needs ink/marker/toner"),
|
|
89
|
+
("job-restartable", "Can be restarted"),
|
|
90
|
+
("job-transforming", "Being transformed into a different format"),
|
|
91
|
+
("printer-stopped", "Printer is stopped"),
|
|
92
|
+
("printer-stopped-partly", "Printer state reason set to 'stopped-partly'"),
|
|
93
|
+
(
|
|
94
|
+
"processing-to-stop-point",
|
|
95
|
+
"Cancelled, but printing already processed pages",
|
|
96
|
+
),
|
|
97
|
+
("queued-in-device", "Queued at the output device"),
|
|
98
|
+
("resources-are-not-ready", "Resources not available to print the job"),
|
|
99
|
+
("service-off-line", "Held because the printer is offline"),
|
|
100
|
+
("submission-interrupted", "Files were not received in full"),
|
|
101
|
+
("unsupported-compression", "Compressed using an unknown algorithm"),
|
|
102
|
+
("unsupported-document-format", "Unsupported format"),
|
|
103
|
+
],
|
|
104
|
+
string="State Reason",
|
|
105
|
+
help="Reason for the current job state.",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
_sql_constraints = [
|
|
109
|
+
(
|
|
110
|
+
"job_id_cups_unique",
|
|
111
|
+
"UNIQUE(job_id_cups, server_id)",
|
|
112
|
+
"The id of the job must be unique per server !",
|
|
113
|
+
)
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
def action_cancel(self):
|
|
117
|
+
self.ensure_one()
|
|
118
|
+
return self.cancel()
|
|
119
|
+
|
|
120
|
+
def cancel(self, purge_job=False):
|
|
121
|
+
for job in self:
|
|
122
|
+
connection = job.server_id._open_connection()
|
|
123
|
+
if not connection:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
connection.cancelJob(job.job_id_cups, purge_job=purge_job)
|
|
127
|
+
|
|
128
|
+
# Update jobs' states info Odoo
|
|
129
|
+
self.mapped("server_id").update_jobs(which="all", first_job_id=job.job_id_cups)
|
|
130
|
+
|
|
131
|
+
return True
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# Copyright (c) 2007 Ferran Pegueroles <ferran@pegueroles.com>
|
|
2
|
+
# Copyright (c) 2009 Albert Cervera i Areny <albert@nan-tic.com>
|
|
3
|
+
# Copyright (C) 2011 Agile Business Group sagl (<http://www.agilebg.com>)
|
|
4
|
+
# Copyright (C) 2011 Domsense srl (<http://www.domsense.com>)
|
|
5
|
+
# Copyright (C) 2013-2014 Camptocamp (<http://www.camptocamp.com>)
|
|
6
|
+
# Copyright (C) 2016 SYLEAM (<http://www.syleam.fr>)
|
|
7
|
+
# Copyright (C) 2023 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
|
|
8
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
9
|
+
|
|
10
|
+
import errno
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
from tempfile import mkstemp
|
|
14
|
+
|
|
15
|
+
from odoo import api, fields, models
|
|
16
|
+
|
|
17
|
+
_logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import cups
|
|
21
|
+
except ImportError:
|
|
22
|
+
_logger.debug("Cannot `import cups`.")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PrintingPrinter(models.Model):
|
|
26
|
+
"""
|
|
27
|
+
Printers
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
_name = "printing.printer"
|
|
31
|
+
_description = "Printer"
|
|
32
|
+
_order = "name"
|
|
33
|
+
|
|
34
|
+
name = fields.Char(required=True, index=True)
|
|
35
|
+
active = fields.Boolean(default=True)
|
|
36
|
+
server_id = fields.Many2one(
|
|
37
|
+
comodel_name="printing.server",
|
|
38
|
+
string="Server",
|
|
39
|
+
required=True,
|
|
40
|
+
help="Server used to access this printer.",
|
|
41
|
+
)
|
|
42
|
+
job_ids = fields.One2many(
|
|
43
|
+
comodel_name="printing.job",
|
|
44
|
+
inverse_name="printer_id",
|
|
45
|
+
string="Jobs",
|
|
46
|
+
help="Jobs printed on this printer.",
|
|
47
|
+
)
|
|
48
|
+
system_name = fields.Char(required=True, index=True)
|
|
49
|
+
default = fields.Boolean(readonly=True)
|
|
50
|
+
status = fields.Selection(
|
|
51
|
+
selection=[
|
|
52
|
+
("unavailable", "Unavailable"),
|
|
53
|
+
("printing", "Printing"),
|
|
54
|
+
("unknown", "Unknown"),
|
|
55
|
+
("available", "Available"),
|
|
56
|
+
("error", "Error"),
|
|
57
|
+
("server-error", "Server Error"),
|
|
58
|
+
],
|
|
59
|
+
required=True,
|
|
60
|
+
readonly=True,
|
|
61
|
+
default="unknown",
|
|
62
|
+
)
|
|
63
|
+
status_message = fields.Char(readonly=True)
|
|
64
|
+
model = fields.Char(readonly=True)
|
|
65
|
+
location = fields.Char(readonly=True)
|
|
66
|
+
uri = fields.Char(string="URI", readonly=True)
|
|
67
|
+
tray_ids = fields.One2many(
|
|
68
|
+
comodel_name="printing.tray", inverse_name="printer_id", string="Paper Sources"
|
|
69
|
+
)
|
|
70
|
+
multi_thread = fields.Boolean(
|
|
71
|
+
compute="_compute_multi_thread", readonly=False, store=True
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@api.depends("server_id.multi_thread")
|
|
75
|
+
def _compute_multi_thread(self):
|
|
76
|
+
for printer in self:
|
|
77
|
+
printer.multi_thread = printer.server_id.multi_thread
|
|
78
|
+
|
|
79
|
+
def _prepare_update_from_cups(self, cups_connection, cups_printer):
|
|
80
|
+
mapping = {3: "available", 4: "printing", 5: "error"}
|
|
81
|
+
cups_vals = {
|
|
82
|
+
"name": self.name or cups_printer["printer-info"],
|
|
83
|
+
"model": cups_printer.get("printer-make-and-model", False),
|
|
84
|
+
"location": cups_printer.get("printer-location", False),
|
|
85
|
+
"uri": cups_printer.get("device-uri", False),
|
|
86
|
+
"status": mapping.get(cups_printer.get("printer-state"), "unknown"),
|
|
87
|
+
"status_message": cups_printer.get("printer-state-message", ""),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# prevent write if the field didn't change
|
|
91
|
+
vals = {
|
|
92
|
+
fieldname: value
|
|
93
|
+
for fieldname, value in cups_vals.items()
|
|
94
|
+
if not self or value != self[fieldname]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
printer_uri = cups_printer["printer-uri-supported"]
|
|
98
|
+
printer_system_name = printer_uri[printer_uri.rfind("/") + 1 :]
|
|
99
|
+
ppd_info = cups_connection.getPPD3(printer_system_name)
|
|
100
|
+
ppd_path = ppd_info[2]
|
|
101
|
+
if not ppd_path:
|
|
102
|
+
return vals
|
|
103
|
+
|
|
104
|
+
ppd = cups.PPD(ppd_path)
|
|
105
|
+
option = ppd.findOption("InputSlot")
|
|
106
|
+
try:
|
|
107
|
+
os.unlink(ppd_path)
|
|
108
|
+
except OSError as err:
|
|
109
|
+
# ENOENT means No such file or directory
|
|
110
|
+
# The file has already been deleted, we can continue the update
|
|
111
|
+
if err.errno != errno.ENOENT:
|
|
112
|
+
raise
|
|
113
|
+
if not option:
|
|
114
|
+
return vals
|
|
115
|
+
|
|
116
|
+
tray_commands = []
|
|
117
|
+
cups_trays = {
|
|
118
|
+
tray_option["choice"]: tray_option["text"] for tray_option in option.choices
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Add new trays
|
|
122
|
+
tray_commands.extend(
|
|
123
|
+
[
|
|
124
|
+
(0, 0, {"name": text, "system_name": choice})
|
|
125
|
+
for choice, text in cups_trays.items()
|
|
126
|
+
if choice not in self.tray_ids.mapped("system_name")
|
|
127
|
+
]
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Remove deleted trays
|
|
131
|
+
tray_commands.extend(
|
|
132
|
+
[
|
|
133
|
+
(2, tray.id)
|
|
134
|
+
for tray in self.tray_ids.filtered(
|
|
135
|
+
lambda record: record.system_name not in cups_trays.keys()
|
|
136
|
+
)
|
|
137
|
+
]
|
|
138
|
+
)
|
|
139
|
+
if tray_commands:
|
|
140
|
+
vals["tray_ids"] = tray_commands
|
|
141
|
+
return vals
|
|
142
|
+
|
|
143
|
+
def print_document(self, report, content, **print_opts):
|
|
144
|
+
"""Print a file
|
|
145
|
+
Format could be pdf, qweb-pdf, raw, ...
|
|
146
|
+
"""
|
|
147
|
+
self.ensure_one()
|
|
148
|
+
fd, file_name = mkstemp()
|
|
149
|
+
if isinstance(content, str):
|
|
150
|
+
content = content.encode()
|
|
151
|
+
try:
|
|
152
|
+
os.write(fd, content)
|
|
153
|
+
finally:
|
|
154
|
+
os.close(fd)
|
|
155
|
+
|
|
156
|
+
return self.print_file(file_name, report=report, **print_opts)
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _set_option_doc_format(report, value):
|
|
160
|
+
return {"raw": "True"} if value == "raw" else {}
|
|
161
|
+
|
|
162
|
+
# Backwards compatibility of builtin used as kwarg
|
|
163
|
+
_set_option_format = _set_option_doc_format
|
|
164
|
+
|
|
165
|
+
def _set_option_tray(self, report, value):
|
|
166
|
+
"""Note we use self here as some older PPD use tray
|
|
167
|
+
rather than InputSlot so we may need to query printer in override"""
|
|
168
|
+
return {"InputSlot": str(value)} if value else {}
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def _set_option_noop(report, value):
|
|
172
|
+
return {}
|
|
173
|
+
|
|
174
|
+
_set_option_action = _set_option_noop
|
|
175
|
+
_set_option_printer = _set_option_noop
|
|
176
|
+
|
|
177
|
+
def print_options(self, report=None, **print_opts):
|
|
178
|
+
options = {}
|
|
179
|
+
for option, value in print_opts.items():
|
|
180
|
+
try:
|
|
181
|
+
options.update(getattr(self, f"_set_option_{option}")(report, value))
|
|
182
|
+
except AttributeError:
|
|
183
|
+
options[option] = str(value)
|
|
184
|
+
return options
|
|
185
|
+
|
|
186
|
+
def print_file(self, file_name, report=None, **print_opts):
|
|
187
|
+
"""Print a file"""
|
|
188
|
+
self.ensure_one()
|
|
189
|
+
title = print_opts.pop("title", file_name)
|
|
190
|
+
connection = self.server_id._open_connection(raise_on_error=True)
|
|
191
|
+
options = self.print_options(report=report, **print_opts)
|
|
192
|
+
|
|
193
|
+
_logger.debug(
|
|
194
|
+
f"Sending job to CUPS printer {self.system_name} on "
|
|
195
|
+
f"{self.server_id.address} with options {options}"
|
|
196
|
+
)
|
|
197
|
+
connection.printFile(self.system_name, file_name, title, options=options)
|
|
198
|
+
_logger.info(f"Printing job: '{file_name}' on {self.server_id.address}")
|
|
199
|
+
try:
|
|
200
|
+
os.remove(file_name)
|
|
201
|
+
except OSError as exc:
|
|
202
|
+
_logger.warning(f"Unable to remove temporary file {file_name}: {exc}")
|
|
203
|
+
return True
|
|
204
|
+
|
|
205
|
+
def set_default(self):
|
|
206
|
+
if not self:
|
|
207
|
+
return
|
|
208
|
+
self.ensure_one()
|
|
209
|
+
default_printers = self.search([("default", "=", True)])
|
|
210
|
+
default_printers.unset_default()
|
|
211
|
+
self.write({"default": True})
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
def unset_default(self):
|
|
215
|
+
self.write({"default": False})
|
|
216
|
+
return True
|
|
217
|
+
|
|
218
|
+
def get_default(self):
|
|
219
|
+
return self.search([("default", "=", True)], limit=1)
|
|
220
|
+
|
|
221
|
+
def action_cancel_all_jobs(self):
|
|
222
|
+
self.ensure_one()
|
|
223
|
+
return self.cancel_all_jobs()
|
|
224
|
+
|
|
225
|
+
def cancel_all_jobs(self, purge_jobs=False):
|
|
226
|
+
for printer in self:
|
|
227
|
+
connection = printer.server_id._open_connection()
|
|
228
|
+
connection.cancelAllJobs(name=printer.system_name, purge_jobs=purge_jobs)
|
|
229
|
+
|
|
230
|
+
# Update jobs' states into Odoo
|
|
231
|
+
self.mapped("server_id").update_jobs(which="completed")
|
|
232
|
+
|
|
233
|
+
return True
|
|
234
|
+
|
|
235
|
+
def enable(self):
|
|
236
|
+
for printer in self:
|
|
237
|
+
connection = printer.server_id._open_connection()
|
|
238
|
+
connection.enablePrinter(printer.system_name)
|
|
239
|
+
|
|
240
|
+
# Update printers' stats into Odoo
|
|
241
|
+
self.mapped("server_id").update_printers()
|
|
242
|
+
|
|
243
|
+
return True
|
|
244
|
+
|
|
245
|
+
def disable(self):
|
|
246
|
+
for printer in self:
|
|
247
|
+
connection = printer.server_id._open_connection()
|
|
248
|
+
connection.disablePrinter(printer.system_name)
|
|
249
|
+
|
|
250
|
+
# Update printers' stats into Odoo
|
|
251
|
+
self.mapped("server_id").update_printers()
|
|
252
|
+
|
|
253
|
+
return True
|
|
254
|
+
|
|
255
|
+
def print_test_page(self):
|
|
256
|
+
for printer in self:
|
|
257
|
+
connection = printer.server_id._open_connection()
|
|
258
|
+
if printer.model == "Local Raw Printer":
|
|
259
|
+
fd, file_name = mkstemp()
|
|
260
|
+
try:
|
|
261
|
+
os.write(fd, b"TEST")
|
|
262
|
+
finally:
|
|
263
|
+
os.close(fd)
|
|
264
|
+
connection.printTestPage(printer.system_name, file=file_name)
|
|
265
|
+
else:
|
|
266
|
+
connection.printTestPage(printer.system_name)
|
|
267
|
+
|
|
268
|
+
self.mapped("server_id").update_jobs(which="completed")
|