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.
Files changed (77) hide show
  1. odoo/addons/base_report_to_printer/README.rst +205 -0
  2. odoo/addons/base_report_to_printer/__init__.py +9 -0
  3. odoo/addons/base_report_to_printer/__manifest__.py +39 -0
  4. odoo/addons/base_report_to_printer/data/neutralize.sql +2 -0
  5. odoo/addons/base_report_to_printer/data/printing_data.xml +27 -0
  6. odoo/addons/base_report_to_printer/i18n/am.po +936 -0
  7. odoo/addons/base_report_to_printer/i18n/base_report_to_printer.pot +932 -0
  8. odoo/addons/base_report_to_printer/i18n/bg.po +939 -0
  9. odoo/addons/base_report_to_printer/i18n/ca.po +936 -0
  10. odoo/addons/base_report_to_printer/i18n/de.po +994 -0
  11. odoo/addons/base_report_to_printer/i18n/el_GR.po +937 -0
  12. odoo/addons/base_report_to_printer/i18n/es.po +1004 -0
  13. odoo/addons/base_report_to_printer/i18n/es_AR.po +987 -0
  14. odoo/addons/base_report_to_printer/i18n/es_ES.po +937 -0
  15. odoo/addons/base_report_to_printer/i18n/fi.po +939 -0
  16. odoo/addons/base_report_to_printer/i18n/fr.po +999 -0
  17. odoo/addons/base_report_to_printer/i18n/gl.po +936 -0
  18. odoo/addons/base_report_to_printer/i18n/hr.po +966 -0
  19. odoo/addons/base_report_to_printer/i18n/hr_HR.po +941 -0
  20. odoo/addons/base_report_to_printer/i18n/it.po +985 -0
  21. odoo/addons/base_report_to_printer/i18n/nl.po +939 -0
  22. odoo/addons/base_report_to_printer/i18n/nl_NL.po +950 -0
  23. odoo/addons/base_report_to_printer/i18n/pt.po +936 -0
  24. odoo/addons/base_report_to_printer/i18n/pt_BR.po +940 -0
  25. odoo/addons/base_report_to_printer/i18n/pt_PT.po +937 -0
  26. odoo/addons/base_report_to_printer/i18n/sl.po +943 -0
  27. odoo/addons/base_report_to_printer/i18n/sv.po +983 -0
  28. odoo/addons/base_report_to_printer/i18n/tr.po +936 -0
  29. odoo/addons/base_report_to_printer/i18n/zh_CN.po +958 -0
  30. odoo/addons/base_report_to_printer/models/__init__.py +8 -0
  31. odoo/addons/base_report_to_printer/models/ir_actions_report.py +253 -0
  32. odoo/addons/base_report_to_printer/models/printing_action.py +26 -0
  33. odoo/addons/base_report_to_printer/models/printing_job.py +131 -0
  34. odoo/addons/base_report_to_printer/models/printing_printer.py +268 -0
  35. odoo/addons/base_report_to_printer/models/printing_report_xml_action.py +48 -0
  36. odoo/addons/base_report_to_printer/models/printing_server.py +275 -0
  37. odoo/addons/base_report_to_printer/models/printing_tray.py +21 -0
  38. odoo/addons/base_report_to_printer/models/res_users.py +55 -0
  39. odoo/addons/base_report_to_printer/readme/CONFIGURE.md +13 -0
  40. odoo/addons/base_report_to_printer/readme/CONTRIBUTORS.md +18 -0
  41. odoo/addons/base_report_to_printer/readme/CREDITS.md +1 -0
  42. odoo/addons/base_report_to_printer/readme/DESCRIPTION.md +27 -0
  43. odoo/addons/base_report_to_printer/readme/HISTORY.md +7 -0
  44. odoo/addons/base_report_to_printer/readme/INSTALL.md +10 -0
  45. odoo/addons/base_report_to_printer/readme/ROADMAP.md +3 -0
  46. odoo/addons/base_report_to_printer/readme/USAGE.md +15 -0
  47. odoo/addons/base_report_to_printer/security/ir.model.access.csv +2 -0
  48. odoo/addons/base_report_to_printer/security/security.xml +151 -0
  49. odoo/addons/base_report_to_printer/static/description/icon.png +0 -0
  50. odoo/addons/base_report_to_printer/static/description/index.html +561 -0
  51. odoo/addons/base_report_to_printer/static/src/js/qweb_action_manager.esm.js +92 -0
  52. odoo/addons/base_report_to_printer/tests/__init__.py +13 -0
  53. odoo/addons/base_report_to_printer/tests/test_ir_actions_report.py +350 -0
  54. odoo/addons/base_report_to_printer/tests/test_printing_job.py +70 -0
  55. odoo/addons/base_report_to_printer/tests/test_printing_printer.py +198 -0
  56. odoo/addons/base_report_to_printer/tests/test_printing_printer_tray.py +256 -0
  57. odoo/addons/base_report_to_printer/tests/test_printing_printer_wizard.py +94 -0
  58. odoo/addons/base_report_to_printer/tests/test_printing_report_xml_action.py +98 -0
  59. odoo/addons/base_report_to_printer/tests/test_printing_server.py +219 -0
  60. odoo/addons/base_report_to_printer/tests/test_printing_tray.py +49 -0
  61. odoo/addons/base_report_to_printer/tests/test_report.py +226 -0
  62. odoo/addons/base_report_to_printer/tests/test_res_users.py +53 -0
  63. odoo/addons/base_report_to_printer/views/ir_actions_report.xml +21 -0
  64. odoo/addons/base_report_to_printer/views/printing_job.xml +46 -0
  65. odoo/addons/base_report_to_printer/views/printing_printer.xml +147 -0
  66. odoo/addons/base_report_to_printer/views/printing_report.xml +39 -0
  67. odoo/addons/base_report_to_printer/views/printing_server.xml +79 -0
  68. odoo/addons/base_report_to_printer/views/res_users.xml +33 -0
  69. odoo/addons/base_report_to_printer/wizards/__init__.py +2 -0
  70. odoo/addons/base_report_to_printer/wizards/print_attachment_report.py +80 -0
  71. odoo/addons/base_report_to_printer/wizards/print_attachment_report.xml +56 -0
  72. odoo/addons/base_report_to_printer/wizards/printing_printer_update_wizard.py +27 -0
  73. odoo/addons/base_report_to_printer/wizards/printing_printer_update_wizard_view.xml +37 -0
  74. odoo_addon_base_report_to_printer-18.0.1.1.6.dist-info/METADATA +222 -0
  75. odoo_addon_base_report_to_printer-18.0.1.1.6.dist-info/RECORD +77 -0
  76. odoo_addon_base_report_to_printer-18.0.1.1.6.dist-info/WHEEL +5 -0
  77. odoo_addon_base_report_to_printer-18.0.1.1.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,8 @@
1
+ from . import ir_actions_report
2
+ from . import printing_action
3
+ from . import printing_job
4
+ from . import printing_printer
5
+ from . import printing_server
6
+ from . import printing_report_xml_action
7
+ from . import printing_tray
8
+ from . import res_users
@@ -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")