From fc29e98dd45f6f8a45d87d48b62d1d2722acbc8b Mon Sep 17 00:00:00 2001 From: Sylvain Garancher Date: Wed, 5 Apr 2017 18:06:16 +0200 Subject: [PATCH] [10.0][MIG] base_report_to_printer (#68) * Set api.multi for action called as `object` on view * Merge syleam printers module into base_report_to_printer (#60) * [IMP] Updated unit tests * [FIX] Fixed renamed attributes * [FIX] Remove deleted fields * [IMP] Add printing.server and printing.job models * [IMP] Allow to cancel all jobs, enable, and disable printers * [IMP] Split the cups part of print_document into a new print_file method * [IMP] Updated cron job to run the action_update_jobs method * [ADD] Add a migration script to create a printing server from configuration * [MIG] Migrate base_report_to_printer to v10.0 Removed deprecated methods on printing.printer (replaced by methods on printing.server) * [IMP] Add wkhtmltopdf in travis configuration file * [FIX] base_report_to_printer: Fix Update Job Cron * Fix API issue with Update Job Cron ** Forward Port from 9.0 * [FIX] Fixed the res.users view The string attribute should not be used as a selector, because it is translatable. * [FIX] Fixed the print_document method of report The new API migration was made to @api.multi because of the "cr, uid, ids" signature, but "ids" was the ids of the records to print here, not the report's ids. Also, the new API version of "get_pdf" get directly the ids of the records to print in the standard module, not a recordset. * [FIX] UI is now (un)blocked only when using qweb-pdf reports in standard addons --- base_report_to_printer/README.rst | 1 + base_report_to_printer/__manifest__.py | 6 +- base_report_to_printer/data/printing_data.xml | 56 ++--- .../9.0.2.0.0/post-10-create_server_record.py | 19 ++ base_report_to_printer/models/__init__.py | 2 + .../models/ir_actions_report_xml.py | 6 +- .../models/printing_action.py | 24 +- base_report_to_printer/models/printing_job.py | 107 +++++++++ .../models/printing_printer.py | 157 ++++++------ .../models/printing_report_xml_action.py | 13 +- .../models/printing_server.py | 224 ++++++++++++++++++ base_report_to_printer/models/report.py | 58 ++--- base_report_to_printer/models/res_users.py | 25 +- base_report_to_printer/report_service.py | 14 +- base_report_to_printer/security/security.xml | 27 +++ .../static/src/js/qweb_action_manager.js | 2 +- base_report_to_printer/tests/__init__.py | 3 + .../tests/test_ir_actions_report_xml.py | 161 ++++++++++++- .../tests/test_printing_job.py | 66 ++++++ .../tests/test_printing_printer.py | 183 ++++++++------ .../tests/test_printing_printer_wizard.py | 26 +- .../tests/test_printing_report_xml_action.py | 58 +++++ .../tests/test_printing_server.py | 213 +++++++++++++++++ base_report_to_printer/tests/test_report.py | 87 ++++++- .../tests/test_res_users.py | 2 +- base_report_to_printer/views/printing_job.xml | 44 ++++ .../views/printing_printer_view.xml | 151 ++++++------ .../views/printing_report_view.xml | 1 + .../views/printing_server.xml | 70 ++++++ .../views/res_users_view.xml | 6 +- .../wizards/printing_printer_update_wizard.py | 47 +--- .../printing_printer_update_wizard_view.xml | 44 ++-- 32 files changed, 1484 insertions(+), 419 deletions(-) create mode 100644 base_report_to_printer/migrations/9.0.2.0.0/post-10-create_server_record.py create mode 100644 base_report_to_printer/models/printing_job.py create mode 100644 base_report_to_printer/models/printing_server.py create mode 100644 base_report_to_printer/tests/test_printing_job.py create mode 100644 base_report_to_printer/tests/test_printing_report_xml_action.py create mode 100644 base_report_to_printer/tests/test_printing_server.py create mode 100644 base_report_to_printer/views/printing_job.xml create mode 100644 base_report_to_printer/views/printing_server.xml diff --git a/base_report_to_printer/README.rst b/base_report_to_printer/README.rst index dd6f146189f..2b63538568f 100644 --- a/base_report_to_printer/README.rst +++ b/base_report_to_printer/README.rst @@ -90,6 +90,7 @@ Contributors * Lionel Sausin * Guewen Baconnier * Dave Lasley +* Sylvain Garancher Maintainer ---------- diff --git a/base_report_to_printer/__manifest__.py b/base_report_to_printer/__manifest__.py index 9d584b6c0d9..a9177316d0c 100644 --- a/base_report_to_printer/__manifest__.py +++ b/base_report_to_printer/__manifest__.py @@ -8,7 +8,7 @@ { 'name': "Report to printer", - 'version': '9.0.1.0.0', + 'version': '10.0.1.0.0', 'category': 'Generic Modules/Base', 'author': "Agile Business Group & Domsense, Pegueroles SCP, NaN," " LasLabs, Odoo Community Association (OCA)", @@ -20,12 +20,14 @@ 'security/security.xml', 'views/assets.xml', 'views/printing_printer_view.xml', + 'views/printing_server.xml', + 'views/printing_job.xml', 'views/printing_report_view.xml', 'views/res_users_view.xml', 'views/ir_actions_report_xml_view.xml', 'wizards/printing_printer_update_wizard_view.xml', ], - 'installable': False, + 'installable': True, 'application': True, 'external_dependencies': { 'python': ['cups'], diff --git a/base_report_to_printer/data/printing_data.xml b/base_report_to_printer/data/printing_data.xml index e6ded50a12d..18fd3992cc9 100644 --- a/base_report_to_printer/data/printing_data.xml +++ b/base_report_to_printer/data/printing_data.xml @@ -1,33 +1,33 @@ - - - - Send to Printer - server - - - Send to Client - client - - - - property_printing_action_id - - - - - Update Printers Status - - - 1 - minutes - -1 - - - - - + + + Send to Printer + server + + + Send to Client + client + + + + property_printing_action_id + + + + + + Update Printers Jobs + + + 1 + minutes + -1 + + + + + diff --git a/base_report_to_printer/migrations/9.0.2.0.0/post-10-create_server_record.py b/base_report_to_printer/migrations/9.0.2.0.0/post-10-create_server_record.py new file mode 100644 index 00000000000..6ae342b265c --- /dev/null +++ b/base_report_to_printer/migrations/9.0.2.0.0/post-10-create_server_record.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016 SYLEAM () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import SUPERUSER_ID, api +from openerp.tools.config import config + +__name__ = 'Create a printing.server record from previous configuration' + + +def migrate(cr, v): + with api.Environment.manage(): + uid = SUPERUSER_ID + env = api.Environment(cr, uid, {}) + env['printing.server'].create({ + 'name': config.get('cups_host', 'localhost'), + 'address': config.get('cups_host', 'localhost'), + 'port': config.get('cups_port', 631), + }) diff --git a/base_report_to_printer/models/__init__.py b/base_report_to_printer/models/__init__.py index 9ed6c150ca4..3736ec1ac5d 100644 --- a/base_report_to_printer/models/__init__.py +++ b/base_report_to_printer/models/__init__.py @@ -2,7 +2,9 @@ from . import ir_actions_report_xml from . import printing_action +from . import printing_job from . import printing_printer +from . import printing_server from . import printing_report_xml_action from . import report from . import res_users diff --git a/base_report_to_printer/models/ir_actions_report_xml.py b/base_report_to_printer/models/ir_actions_report_xml.py index 511915daa78..f45867ada55 100644 --- a/base_report_to_printer/models/ir_actions_report_xml.py +++ b/base_report_to_printer/models/ir_actions_report_xml.py @@ -6,7 +6,7 @@ # Copyright (C) 2013-2014 Camptocamp () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp import models, fields, api +from odoo import models, fields, api class IrActionsReportXml(models.Model): @@ -73,8 +73,8 @@ def behaviour(self): # Retrieve report default values report_action = report.property_printing_action_id - if report_action and report_action.type != 'user_default': - action = report_action.type + if report_action and report_action.action_type != 'user_default': + action = report_action.action_type if report.printing_printer_id: printer = report.printing_printer_id diff --git a/base_report_to_printer/models/printing_action.py b/base_report_to_printer/models/printing_action.py index 0781b9978ea..39c276600c7 100644 --- a/base_report_to_printer/models/printing_action.py +++ b/base_report_to_printer/models/printing_action.py @@ -6,23 +6,25 @@ # Copyright (C) 2013-2014 Camptocamp () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp import models, fields, api - - -@api.model -def _available_action_types(self): - return [('server', 'Send to Printer'), - ('client', 'Send to Client'), - ('user_default', "Use user's defaults"), - ] +from odoo import models, fields, api class PrintingAction(models.Model): _name = 'printing.action' _description = 'Print Job Action' + @api.model + def _available_action_types(self): + return [ + ('server', 'Send to Printer'), + ('client', 'Send to Client'), + ('user_default', "Use user's defaults"), + ] + name = fields.Char(required=True) - type = fields.Selection( - lambda s: _available_action_types(s), + action_type = fields.Selection( + selection=_available_action_types, + string='Type', required=True, + oldname='type' ) diff --git a/base_report_to_printer/models/printing_job.py b/base_report_to_printer/models/printing_job.py new file mode 100644 index 00000000000..63fbde96829 --- /dev/null +++ b/base_report_to_printer/models/printing_job.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016 SYLEAM () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from odoo import models, fields, api + +_logger = logging.getLogger(__name__) + + +class PrintingJob(models.Model): + _name = 'printing.job' + _description = 'Printing Job' + _order = 'job_id_cups DESC' + + name = fields.Char(help='Job name.') + active = fields.Boolean( + default=True, help='Unchecked if the job is purged from cups.') + job_id_cups = fields.Integer( + string='Job ID', required=True, + help='CUPS id for this job.') + server_id = fields.Many2one( + comodel_name='printing.server', string='Server', + related='printer_id.server_id', store=True, + help='Server which hosts this job.') + printer_id = fields.Many2one( + comodel_name='printing.printer', string='Printer', required=True, + ondelete='cascade', help='Printer used for this job.') + job_media_progress = fields.Integer( + string='Media Progress', required=True, + help='Percentage of progress for this job.') + time_at_creation = fields.Datetime( + required=True, help='Date and time of creation for this job.') + time_at_processing = fields.Datetime( + help='Date and time of process for this job.') + time_at_completed = fields.Datetime( + help='Date and time of completion for this job.') + job_state = fields.Selection(selection=[ + ('pending', 'Pending'), + ('pending held', 'Pending Held'), + ('processing', 'Processing'), + ('processing stopped', 'Processing Stopped'), + ('canceled', 'Canceled'), + ('aborted', 'Aborted'), + ('completed', 'Completed'), + ('unknown', 'Unknown'), + ], string='State', help='Current state of the job.') + job_state_reason = fields.Selection(selection=[ + ('none', 'No reason'), + ('aborted-by-system', 'Aborted by the system'), + ('compression-error', 'Error in the compressed data'), + ('document-access-error', 'The URI cannot be accessed'), + ('document-format-error', 'Error in the document'), + ('job-canceled-at-device', 'Cancelled at the device'), + ('job-canceled-by-operator', 'Cancelled by the printer operator'), + ('job-canceled-by-user', 'Cancelled by the user'), + ('job-completed-successfully', 'Completed successfully'), + ('job-completed-with-errors', 'Completed with some errors'), + ('job-completed(with-warnings', 'Completed with some warnings'), + ('job-data-insufficient', 'No data has been received'), + ('job-hold-until-specified', 'Currently held'), + ('job-incoming', 'Files are currently being received'), + ('job-interpreting', 'Currently being interpreted'), + ('job-outgoing', 'Currently being sent to the printer'), + ('job-printing', 'Currently printing'), + ('job-queued', 'Queued for printing'), + ('job-queued-for-marker', 'Printer needs ink/marker/toner'), + ('job-restartable', 'Can be restarted'), + ('job-transforming', 'Being transformed into a different format'), + ('printer-stopped', 'Printer is stopped'), + ('printer-stopped-partly', + 'Printer state reason set to \'stopped-partly\''), + ('processing-to-stop-point', + 'Cancelled, but printing already processed pages'), + ('queued-in-device', 'Queued at the output device'), + ('resources-are-not-ready', + 'Resources not available to print the job'), + ('service-off-line', 'Held because the printer is offline'), + ('submission-interrupted', 'Files were not received in full'), + ('unsupported-compression', 'Compressed using an unknown algorithm'), + ('unsupported-document-format', 'Unsupported format'), + ], string='State Reason', help='Reason for the current job state.') + + _sql_constraints = [ + ('job_id_cups_unique', 'UNIQUE(job_id_cups, server_id)', + 'The id of the job must be unique per server !'), + ] + + @api.multi + def action_cancel(self): + self.ensure_one() + return self.cancel() + + @api.multi + def cancel(self, purge_job=False): + for job in self: + connection = job.server_id._open_connection() + if not connection: + continue + + connection.cancelJob(job.job_id_cups, purge_job=purge_job) + + # Update jobs' states info Odoo + self.mapped('server_id').update_jobs( + which='all', first_job_id=job.job_id_cups) + + return True diff --git a/base_report_to_printer/models/printing_printer.py b/base_report_to_printer/models/printing_printer.py index 9a6b8fb7501..c50693362d9 100644 --- a/base_report_to_printer/models/printing_printer.py +++ b/base_report_to_printer/models/printing_printer.py @@ -4,6 +4,7 @@ # Copyright (C) 2011 Agile Business Group sagl () # Copyright (C) 2011 Domsense srl () # Copyright (C) 2013-2014 Camptocamp () +# Copyright (C) 2016 SYLEAM () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging @@ -11,24 +12,12 @@ import os from tempfile import mkstemp -from openerp import models, fields, api, _ -from openerp.exceptions import UserError -from openerp.tools.config import config +from odoo import models, fields, api _logger = logging.getLogger(__name__) -try: - import cups -except ImportError: - _logger.debug('Cannot `import cups`.') - - -CUPS_HOST = config.get('cups_host', 'localhost') -CUPS_PORT = int(config.get('cups_port', 631)) # config.get returns a string - - class PrintingPrinter(models.Model): """ Printers @@ -38,43 +27,32 @@ class PrintingPrinter(models.Model): _description = 'Printer' _order = 'name' - name = fields.Char(required=True, select=True) - system_name = fields.Char(required=True, select=True) + name = fields.Char(required=True, index=True) + server_id = fields.Many2one( + comodel_name='printing.server', string='Server', required=True, + help='Server used to access this printer.') + job_ids = fields.One2many( + comodel_name='printing.job', inverse_name='printer_id', string='Jobs', + help='Jobs printed on this printer.') + system_name = fields.Char(required=True, index=True) default = fields.Boolean(readonly=True) - status = fields.Selection([('unavailable', 'Unavailable'), - ('printing', 'Printing'), - ('unknown', 'Unknown'), - ('available', 'Available'), - ('error', 'Error'), - ('server-error', 'Server Error')], - required=True, - readonly=True, - default='unknown') + status = fields.Selection( + selection=[ + ('unavailable', 'Unavailable'), + ('printing', 'Printing'), + ('unknown', 'Unknown'), + ('available', 'Available'), + ('error', 'Error'), + ('server-error', 'Server Error'), + ], + required=True, + readonly=True, + default='unknown') status_message = fields.Char(readonly=True) model = fields.Char(readonly=True) location = fields.Char(readonly=True) uri = fields.Char(string='URI', readonly=True) - @api.model - def update_printers_status(self, domain=None): - if domain is None: - domain = [] - printer_recs = self.search(domain) - try: - connection = cups.Connection(CUPS_HOST, CUPS_PORT) - printers = connection.getPrinters() - except: - printer_recs.write({'status': 'server-error'}) - else: - for printer in printer_recs: - cups_printer = printers.get(printer.system_name) - if cups_printer: - printer.update_from_cups(connection, cups_printer) - else: - # not in cups list - printer.status = 'unavailable' - return True - @api.multi def _prepare_update_from_cups(self, cups_connection, cups_printer): mapping = { @@ -83,29 +61,18 @@ def _prepare_update_from_cups(self, cups_connection, cups_printer): 5: 'error' } vals = { + 'name': cups_printer['printer-info'], 'model': cups_printer.get('printer-make-and-model', False), 'location': cups_printer.get('printer-location', False), 'uri': cups_printer.get('device-uri', False), - 'status': mapping.get(cups_printer['printer-state'], 'unknown'), + 'status': mapping.get(cups_printer.get( + 'printer-state'), 'unknown'), + 'status_message': cups_printer.get('printer-state-message', ''), } return vals @api.multi - def update_from_cups(self, cups_connection, cups_printer): - """ Update a printer from the information returned by cups. - - :param cups_connection: connection to CUPS, may be used when the - method is overriden (e.g. in printer_tray) - :param cups_printer: dict of information returned by CUPS for the - current printer - """ - self.ensure_one() - vals = self._prepare_update_from_cups(cups_connection, cups_printer) - if any(self[name] != value for name, value in vals.iteritems()): - self.write(vals) - - @api.multi - def print_options(self, report, format, copies=1): + def print_options(self, report=None, format=None, copies=1): """ Hook to set print options """ options = {} if format == 'raw': @@ -128,29 +95,28 @@ def print_document(self, report, content, format, copies=1): finally: os.close(fd) - try: - _logger.debug( - 'Starting to connect to CUPS on %s:%s' - % (CUPS_HOST, CUPS_PORT)) - connection = cups.Connection(CUPS_HOST, CUPS_PORT) - _logger.debug('Connection to CUPS successfull') - except: - raise UserError( - _("Failed to connect to the CUPS server on %s:%s. " - "Check that the CUPS server is running and that " - "you can reach it from the Odoo server.") - % (CUPS_HOST, CUPS_PORT)) - - options = self.print_options(report, format, copies) + return self.print_file(file_name, report=report, copies=copies) + + @api.multi + def print_file(self, file_name, report=None, copies=1): + """ Print a file """ + self.ensure_one() + + connection = self.server_id._open_connection(raise_on_error=True) + options = self.print_options( + report=report, format=format, copies=copies) _logger.debug( 'Sending job to CUPS printer %s on %s' - % (self.system_name, CUPS_HOST)) + % (self.system_name, self.server_id.address)) connection.printFile(self.system_name, file_name, file_name, options=options) - _logger.info("Printing job: '%s' on %s" % (file_name, CUPS_HOST)) + _logger.info("Printing job: '%s' on %s" % ( + file_name, + self.server_id.address, + )) return True @api.multi @@ -166,3 +132,42 @@ def set_default(self): @api.multi def get_default(self): return self.search([('default', '=', True)], limit=1) + + @api.multi + def action_cancel_all_jobs(self): + self.ensure_one() + return self.cancel_all_jobs() + + @api.multi + def cancel_all_jobs(self, purge_jobs=False): + for printer in self: + connection = printer.server_id._open_connection() + connection.cancelAllJobs( + name=printer.system_name, purge_jobs=purge_jobs) + + # Update jobs' states into Odoo + self.mapped('server_id').update_jobs(which='completed') + + return True + + @api.multi + def enable(self): + for printer in self: + connection = printer.server_id._open_connection() + connection.enablePrinter(printer.system_name) + + # Update printers' stats into Odoo + self.mapped('server_id').update_printers() + + return True + + @api.multi + def disable(self): + for printer in self: + connection = printer.server_id._open_connection() + connection.disablePrinter(printer.system_name) + + # Update printers' stats into Odoo + self.mapped('server_id').update_printers() + + return True diff --git a/base_report_to_printer/models/printing_report_xml_action.py b/base_report_to_printer/models/printing_report_xml_action.py index 2a67c9bbcea..fc90155772b 100644 --- a/base_report_to_printer/models/printing_report_xml_action.py +++ b/base_report_to_printer/models/printing_report_xml_action.py @@ -6,9 +6,7 @@ # Copyright (C) 2013-2014 Camptocamp () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp import models, fields, api - -from .printing_action import _available_action_types +from odoo import models, fields, api class PrintingReportXmlAction(models.Model): @@ -24,7 +22,7 @@ class PrintingReportXmlAction(models.Model): required=True, ondelete='cascade') action = fields.Selection( - lambda s: _available_action_types(s), + selection=lambda s: s.env['printing.action']._available_action_types(), required=True, ) printer_id = fields.Many2one(comodel_name='printing.printer', @@ -34,6 +32,7 @@ class PrintingReportXmlAction(models.Model): def behaviour(self): if not self: return {} - return {'action': self.action, - 'printer': self.printer_id, - } + return { + 'action': self.action, + 'printer': self.printer_id, + } diff --git a/base_report_to_printer/models/printing_server.py b/base_report_to_printer/models/printing_server.py new file mode 100644 index 00000000000..3e7d59faf00 --- /dev/null +++ b/base_report_to_printer/models/printing_server.py @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016 SYLEAM () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from datetime import datetime +from odoo import models, fields, api, exceptions, _ + +_logger = logging.getLogger(__name__) + + +try: + import cups +except ImportError: + _logger.debug('Cannot `import cups`.') + + +class PrintingServer(models.Model): + _name = 'printing.server' + _description = 'Printing server' + + name = fields.Char( + default='Localhost', required=True, help='Name of the server.') + address = fields.Char( + default='localhost', required=True, + help='IP address or hostname of the server') + port = fields.Integer( + default=631, required=True, help='Port of the server.') + active = fields.Boolean( + default=True, help='If checked, this server is useable.') + printer_ids = fields.One2many( + comodel_name='printing.printer', inverse_name='server_id', + string='Printers List', + help='List of printers available on this server.') + + @api.multi + def _open_connection(self, raise_on_error=False): + self.ensure_one() + connection = False + try: + connection = cups.Connection(host=self.address, port=self.port) + except: + message = _("Failed to connect to the CUPS server on %s:%s. " + "Check that the CUPS server is running and that " + "you can reach it from the Odoo server.") % ( + self.address, self.port) + _logger.warning(message) + if raise_on_error: + raise exceptions.UserError(message) + + return connection + + @api.multi + def action_update_printers(self): + return self.update_printers() + + @api.multi + def update_printers(self, domain=None, raise_on_error=False): + if domain is None: + domain = [] + + servers = self + if not self: + servers = self.search(domain) + + res = True + for server in servers: + connection = server._open_connection(raise_on_error=raise_on_error) + if not connection: + server.printer_ids.write({'status': 'server-error'}) + res = False + continue + + # Update Printers + printers = connection.getPrinters() + existing_printers = dict([ + (printer.system_name, printer) + for printer in server.printer_ids + ]) + updated_printers = [] + for name, printer_info in printers.iteritems(): + printer = self.env['printing.printer'] + if name in existing_printers: + printer = existing_printers[name] + + printer_values = printer._prepare_update_from_cups( + connection, printer_info) + printer_values.update( + system_name=name, + server_id=server.id, + ) + updated_printers.append(name) + if not printer: + printer.create(printer_values) + else: + printer.write(printer_values) + + # Set printers not found as unavailable + server.printer_ids.filtered( + lambda record: record.system_name not in updated_printers)\ + .write({'status': 'unavailable'}) + + return res + + @api.model + def action_update_jobs(self): + if not self: + self = self.search([]) + return self.update_jobs() + + @api.multi + def update_jobs(self, which='all', first_job_id=-1): + job_obj = self.env['printing.job'] + printer_obj = self.env['printing.printer'] + + mapping = { + 3: 'pending', + 4: 'pending held', + 5: 'processing', + 6: 'processing stopped', + 7: 'canceled', + 8: 'aborted', + 9: 'completed', + } + + # Update printers list, to ensure that jobs printers will be in Odoo + self.update_printers() + + for server in self: + connection = server._open_connection() + if not connection: + continue + + # Retrieve asked job data + jobs_data = connection.getJobs( + which_jobs=which, first_job_id=first_job_id, + requested_attributes=[ + 'job-name', + 'job-id', + 'printer-uri', + 'job-media-progress', + 'time-at-creation', + 'job-state', + 'job-state-reasons', + 'time-at-processing', + 'time-at-completed', + ]) + + # Retrieve known uncompleted jobs data to update them + if which == 'not-completed': + oldest_uncompleted_job = job_obj.search([ + ('job_state', 'not in', ( + 'canceled', + 'aborted', + 'completed', + )), + ], limit=1, order='job_id_cups') + if oldest_uncompleted_job: + jobs_data.update(connection.getJobs( + which_jobs='completed', + first_job_id=oldest_uncompleted_job.job_id_cups, + requested_attributes=[ + 'job-name', + 'job-id', + 'printer-uri', + 'job-media-progress', + 'time-at-creation', + 'job-state', + 'job-state-reasons', + 'time-at-processing', + 'time-at-completed', + ])) + + all_cups_job_ids = set() + for cups_job_id, job_data in jobs_data.items(): + all_cups_job_ids.add(cups_job_id) + jobs = job_obj.with_context(active_test=False).search([ + ('job_id_cups', '=', cups_job_id), + ('server_id', '=', server.id), + ]) + job_values = { + 'name': job_data.get('job-name', ''), + 'active': True, + 'job_id_cups': cups_job_id, + 'job_media_progress': job_data.get( + 'job-media-progress', 0), + 'job_state': mapping.get( + job_data.get('job-state'), 'unknown'), + 'job_state_reason': job_data.get('job-state-reasons', ''), + 'time_at_creation': fields.Datetime.to_string( + datetime.fromtimestamp(job_data.get( + 'time-at-creation', 0))), + 'time_at_processing': job_data.get( + 'time-at-processing', 0) and fields.Datetime.to_string( + datetime.fromtimestamp(job_data.get( + 'time-at-processing', 0))), + 'time_at_completed': job_data.get( + 'time-at-completed', 0) and fields.Datetime.to_string( + datetime.fromtimestamp(job_data.get( + 'time-at-completed', 0))), + } + + # Search for the printer in Odoo + printer_uri = job_data['printer-uri'] + printer_system_name = printer_uri[printer_uri.rfind('/') + 1:] + printer = printer_obj.search([ + ('server_id', '=', server.id), + ('system_name', '=', printer_system_name), + ], limit=1) + job_values['printer_id'] = printer.id + + if jobs: + jobs.write(job_values) + else: + job_obj.create(job_values) + + # Deactive purged jobs + if which == 'all' and first_job_id == -1: + purged_jobs = job_obj.search([ + ('job_id_cups', 'not in', list(all_cups_job_ids)), + ]) + purged_jobs.write({'active': False}) + + return True diff --git a/base_report_to_printer/models/report.py b/base_report_to_printer/models/report.py index 3ff064a5439..28ca2980cc2 100644 --- a/base_report_to_printer/models/report.py +++ b/base_report_to_printer/models/report.py @@ -2,34 +2,25 @@ # Copyright (c) 2014 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp import models, exceptions, _, api +from odoo import models, exceptions, _, api class Report(models.Model): _inherit = 'report' - @api.multi - def print_document(self, report_name, html=None, data=None): + @api.model + def print_document(self, record_ids, report_name, html=None, data=None): """ Print a document, do not return the document file """ - res = [] - context = self.env.context - if context is None: - context = self.env['res.users'].context_get() - local_context = context.copy() - local_context['must_skip_send_to_printer'] = True - for rec_id in self.with_context(local_context): - document = rec_id.get_pdf(report_name, html=html, data=data) - report = self._get_report_from_name(report_name) - behaviour = report.behaviour()[report.id] - printer = behaviour['printer'] - if not printer: - raise exceptions.Warning( - _('No printer configured to print this report.') - ) - res.append( - printer.print_document(report, document, report.report_type) + document = self.with_context(must_skip_send_to_printer=True).get_pdf( + record_ids, report_name, html=html, data=data) + report = self._get_report_from_name(report_name) + behaviour = report.behaviour()[report.id] + printer = behaviour['printer'] + if not printer: + raise exceptions.Warning( + _('No printer configured to print this report.') ) - return all(res) + return printer.print_document(report, document, report.report_type) @api.multi def _can_print_report(self, behaviour, printer, document): @@ -44,29 +35,22 @@ def _can_print_report(self, behaviour, printer, document): return True return False - @api.v7 - def get_pdf(self, cr, uid, ids, report_name, html=None, - data=None, context=None): + @api.model + def get_pdf(self, docids, report_name, html=None, data=None): """ Generate a PDF and returns it. If the action configured on the report is server, it prints the generated document as well. """ - document = super(Report, self).get_pdf(cr, uid, ids, report_name, - html=html, data=data, - context=context) - report = self._get_report_from_name(cr, uid, report_name) + document = super(Report, self).get_pdf( + docids, report_name, html=html, data=data) + + report = self._get_report_from_name(report_name) behaviour = report.behaviour()[report.id] printer = behaviour['printer'] - can_print_report = self._can_print_report(cr, uid, ids, - behaviour, printer, document, - context=context) + can_print_report = self._can_print_report(behaviour, printer, document) + if can_print_report: printer.print_document(report, document, report.report_type) - return document - @api.v8 - def get_pdf(self, records, report_name, html=None, data=None): - return self._model.get_pdf(self._cr, self._uid, - records.ids, report_name, - html=html, data=data, context=self._context) + return document diff --git a/base_report_to_printer/models/res_users.py b/base_report_to_printer/models/res_users.py index 1787e819813..35be34f97a1 100644 --- a/base_report_to_printer/models/res_users.py +++ b/base_report_to_printer/models/res_users.py @@ -6,26 +6,23 @@ # Copyright (C) 2013-2014 Camptocamp () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp import models, fields, api - -from .printing_action import _available_action_types +from odoo import models, fields, api class ResUsers(models.Model): _inherit = 'res.users' + @api.model + def _user_available_action_types(self): + return [ + (code, string) + for code, string + in self.env['printing.action']._available_action_types() + if code != 'user_default' + ] + printing_action = fields.Selection( - lambda s: s._user_available_action_types(), + selection=_user_available_action_types, ) printing_printer_id = fields.Many2one(comodel_name='printing.printer', string='Default Printer') - - @api.model - def _available_action_types(self): - return _available_action_types(self) - - @api.model - def _user_available_action_types(self): - return [(code, string) for code, string - in self._available_action_types() - if code != 'user_default'] diff --git a/base_report_to_printer/report_service.py b/base_report_to_printer/report_service.py index 66fcfd2c04f..4237870f7f6 100644 --- a/base_report_to_printer/report_service.py +++ b/base_report_to_printer/report_service.py @@ -6,11 +6,11 @@ # Copyright (C) 2013-2014 Camptocamp () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -import openerp +import odoo -from openerp.service.report import self_reports +from odoo.service.report import self_reports -original_exp_report = openerp.service.report.exp_report +original_exp_report = odoo.service.report.exp_report def exp_report(db, uid, object, ids, datas=None, context=None): @@ -22,15 +22,15 @@ def exp_report(db, uid, object, ids, datas=None, context=None): return res -openerp.service.report.exp_report = exp_report +odoo.service.report.exp_report = exp_report -original_exp_report_get = openerp.service.report.exp_report_get +original_exp_report_get = odoo.service.report.exp_report_get def exp_report_get(db, uid, report_id): # First we need to know if the module is installed - registry = openerp.registry(db) + registry = odoo.registry(db) if registry.get('printing.printer'): cr = registry.cursor() try: @@ -72,4 +72,4 @@ def exp_report_get(db, uid, report_id): return original_exp_report_get(db, uid, report_id) -openerp.service.report.exp_report_get = exp_report_get +odoo.service.report.exp_report_get = exp_report_get diff --git a/base_report_to_printer/security/security.xml b/base_report_to_printer/security/security.xml index 262209d763e..5bf2761b7ce 100644 --- a/base_report_to_printer/security/security.xml +++ b/base_report_to_printer/security/security.xml @@ -8,6 +8,15 @@ Printing / Print User + + Printing Server Manager + + + + + + + Printing Printer Manager @@ -38,6 +47,15 @@ + + Printing Server User + + + + + + + Printing Printer User @@ -47,6 +65,15 @@ + + Printing Job User + + + + + + + Printing Action User diff --git a/base_report_to_printer/static/src/js/qweb_action_manager.js b/base_report_to_printer/static/src/js/qweb_action_manager.js index 68c34601a55..794e24b6f29 100644 --- a/base_report_to_printer/static/src/js/qweb_action_manager.js +++ b/base_report_to_printer/static/src/js/qweb_action_manager.js @@ -8,13 +8,13 @@ odoo.define('base_report_to_printer.print', function(require) { ActionManager.include({ ir_actions_report_xml: function(action, options) { - framework.blockUI(); action = _.clone(action); var _t = core._t; var self = this; var _super = this._super; if ('report_type' in action && action.report_type === 'qweb-pdf') { + framework.blockUI(); new Model('ir.actions.report.xml') .call('print_action_for_report_name', [action.report_name]) .then(function(print_action){ diff --git a/base_report_to_printer/tests/__init__.py b/base_report_to_printer/tests/__init__.py index 90bcf20fd65..a43edf141ea 100644 --- a/base_report_to_printer/tests/__init__.py +++ b/base_report_to_printer/tests/__init__.py @@ -2,8 +2,11 @@ # Copyright 2016 LasLabs Inc. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import test_printing_job from . import test_printing_printer +from . import test_printing_server from . import test_report from . import test_res_users from . import test_ir_actions_report_xml from . import test_printing_printer_wizard +from . import test_printing_report_xml_action diff --git a/base_report_to_printer/tests/test_ir_actions_report_xml.py b/base_report_to_printer/tests/test_ir_actions_report_xml.py index 49fd70d87ad..442f9603b0b 100644 --- a/base_report_to_printer/tests/test_ir_actions_report_xml.py +++ b/base_report_to_printer/tests/test_ir_actions_report_xml.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- # Copyright 2016 LasLabs Inc. +# Copyright 2016 SYLEAM # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import mock -from openerp.tests.common import TransactionCase +from odoo.tests.common import TransactionCase class TestIrActionsReportXml(TransactionCase): @@ -14,8 +15,34 @@ def setUp(self): self.Model = self.env['ir.actions.report.xml'] self.vals = {} - def new_record(self): - return self.Model.create(self.vals) + self.report = self.env['ir.actions.report.xml'].search([], limit=1) + self.server = self.env['printing.server'].create({}) + + def new_action(self): + return self.env['printing.action'].create({ + 'name': 'Printing Action', + 'action_type': 'server', + }) + + def new_printing_action(self): + return self.env['printing.report.xml.action'].create({ + 'report_id': self.report.id, + 'user_id': self.env.ref('base.user_demo').id, + 'action': 'server', + }) + + def new_printer(self): + return self.env['printing.printer'].create({ + 'name': 'Printer', + 'server_id': self.server.id, + 'system_name': 'Sys Name', + 'default': True, + 'status': 'unknown', + 'status_message': 'Msg', + 'model': 'res.users', + 'location': 'Location', + 'uri': 'URI', + }) def test_print_action_for_report_name_gets_report(self): """ It should get report by name """ @@ -51,3 +78,131 @@ def test_print_action_for_report_name_returns_if_report(self): expect, res, 'Expect %s, Got %s' % (expect, res), ) + + def test_behaviour_default_values(self): + """ It should return the default action and printer """ + report = self.Model.search([], limit=1) + self.env.user.printing_action = False + self.env.user.printing_printer_id = False + report.property_printing_action_id = False + report.printing_printer_id = False + self.assertEqual(report.behaviour(), { + report.id: { + 'action': 'client', + 'printer': self.env['printing.printer'], + }, + }) + + def test_behaviour_user_values(self): + """ It should return the action and printer from user """ + report = self.Model.search([], limit=1) + self.env.user.printing_action = 'client' + self.env.user.printing_printer_id = self.new_printer() + self.assertEqual(report.behaviour(), { + report.id: { + 'action': 'client', + 'printer': self.env.user.printing_printer_id, + }, + }) + + def test_behaviour_report_values(self): + """ It should return the action and printer from report """ + report = self.Model.search([], limit=1) + self.env.user.printing_action = 'client' + report.property_printing_action_id = self.new_action() + report.printing_printer_id = self.new_printer() + self.assertEqual(report.behaviour(), { + report.id: { + 'action': report.property_printing_action_id.action_type, + 'printer': report.printing_printer_id, + }, + }) + + def test_behaviour_user_action(self): + """ It should return the action and printer from user action""" + report = self.Model.search([], limit=1) + self.env.user.printing_action = 'client' + report.property_printing_action_id.action_type = 'user_default' + self.assertEqual(report.behaviour(), { + report.id: { + 'action': 'client', + 'printer': report.printing_printer_id, + }, + }) + + def test_behaviour_printing_action_on_wrong_user(self): + """ It should return the action and printer ignoring printing action + """ + report = self.Model.search([], limit=1) + self.env.user.printing_action = 'client' + printing_action = self.new_printing_action() + printing_action.user_id = self.env['res.users'].search([ + ('id', '!=', self.env.user.id), + ], limit=1) + self.assertEqual(report.behaviour(), { + report.id: { + 'action': 'client', + 'printer': report.printing_printer_id, + }, + }) + + def test_behaviour_printing_action_on_wrong_report(self): + """ It should return the action and printer ignoring printing action + """ + report = self.Model.search([], limit=1) + self.env.user.printing_action = 'client' + printing_action = self.new_printing_action() + printing_action.user_id = self.env.user + printing_action.report_id = self.env['ir.actions.report.xml'].search([ + ('id', '!=', report.id), + ], limit=1) + self.assertEqual(report.behaviour(), { + report.id: { + 'action': 'client', + 'printer': report.printing_printer_id, + }, + }) + + def test_behaviour_printing_action_with_no_printer(self): + """ It should return the action from printing action and printer from other + """ + report = self.Model.search([], limit=1) + self.env.user.printing_action = 'client' + printing_action = self.new_printing_action() + printing_action.user_id = self.env.user + printing_action.report_id = report + self.assertEqual(report.behaviour(), { + report.id: { + 'action': printing_action.action, + 'printer': report.printing_printer_id, + }, + }) + + def test_behaviour_printing_action_with_printer(self): + """ It should return the action and printer from printing action """ + report = self.Model.search([], limit=1) + self.env.user.printing_action = 'client' + printing_action = self.new_printing_action() + printing_action.user_id = self.env.user + printing_action.printer_id = self.new_printer() + self.assertEqual(report.behaviour(), { + report.id: { + 'action': printing_action.action, + 'printer': printing_action.printer_id, + }, + }) + + def test_behaviour_printing_action_user_defaults(self): + """ It should return the action and printer from user with printing action + """ + report = self.Model.search([], limit=1) + self.env.user.printing_action = 'client' + printing_action = self.new_printing_action() + printing_action.user_id = self.env.user + printing_action.action = 'user_default' + self.assertEqual(report.behaviour(), { + report.id: { + 'action': 'client', + 'printer': report.printing_printer_id, + }, + }) diff --git a/base_report_to_printer/tests/test_printing_job.py b/base_report_to_printer/tests/test_printing_job.py new file mode 100644 index 00000000000..6a824223128 --- /dev/null +++ b/base_report_to_printer/tests/test_printing_job.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import mock + +from odoo import fields +from odoo.tests.common import TransactionCase + + +model = 'odoo.addons.base_report_to_printer.models.printing_server' + + +class TestPrintingJob(TransactionCase): + + def setUp(self): + super(TestPrintingJob, self).setUp() + self.Model = self.env['printing.server'] + self.server = self.Model.create({}) + self.printer_vals = { + 'name': 'Printer', + 'server_id': self.server.id, + 'system_name': 'Sys Name', + 'default': True, + 'status': 'unknown', + 'status_message': 'Msg', + 'model': 'res.users', + 'location': 'Location', + 'uri': 'URI', + } + self.job_vals = { + 'server_id': self.server.id, + 'job_id_cups': 1, + 'job_media_progress': 0, + 'time_at_creation': fields.Datetime.now(), + } + + def new_printer(self): + return self.env['printing.printer'].create(self.printer_vals) + + def new_job(self, printer, vals=None): + values = self.job_vals + if vals is not None: + values.update(vals) + values['printer_id'] = printer.id + return self.env['printing.job'].create(values) + + @mock.patch('%s.cups' % model) + def test_cancel_job_error(self, cups): + """ It should catch any exception from CUPS and update status """ + cups.Connection.side_effect = Exception + printer = self.new_printer() + job = self.new_job(printer, {'job_id_cups': 2}) + job.action_cancel() + cups.Connection.side_effect = None + self.assertEquals(cups.Connection().cancelJob.call_count, 0) + + @mock.patch('%s.cups' % model) + def test_cancel_job(self, cups): + """ It should catch any exception from CUPS and update status """ + printer = self.new_printer() + job = self.new_job(printer) + job.cancel() + cups.Connection().cancelJob.assert_called_once_with( + job.job_id_cups, purge_job=False, + ) diff --git a/base_report_to_printer/tests/test_printing_printer.py b/base_report_to_printer/tests/test_printing_printer.py index 7801fe8afc6..f98347201b2 100644 --- a/base_report_to_printer/tests/test_printing_printer.py +++ b/base_report_to_printer/tests/test_printing_printer.py @@ -2,17 +2,15 @@ # Copyright 2016 LasLabs Inc. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import tempfile import mock -from openerp.tests.common import TransactionCase +from odoo.exceptions import UserError +from odoo.tests.common import TransactionCase -from openerp.addons.base_report_to_printer.models.printing_printer import ( - CUPS_HOST, - CUPS_PORT, -) - -model = 'openerp.addons.base_report_to_printer.models.printing_printer' +model = 'odoo.addons.base_report_to_printer.models.printing_printer' +server_model = 'odoo.addons.base_report_to_printer.models.printing_server' class TestPrintingPrinter(TransactionCase): @@ -20,8 +18,11 @@ class TestPrintingPrinter(TransactionCase): def setUp(self): super(TestPrintingPrinter, self).setUp() self.Model = self.env['printing.printer'] + self.ServerModel = self.env['printing.server'] + self.server = self.env['printing.server'].create({}) self.printer_vals = { 'name': 'Printer', + 'server_id': self.server.id, 'system_name': 'Sys Name', 'default': True, 'status': 'unknown', @@ -34,74 +35,110 @@ def setUp(self): def new_record(self): return self.Model.create(self.printer_vals) - @mock.patch('%s.cups' % model) - def test_update_printers_status_error(self, cups): - """ It should catch any exception from CUPS and update status """ + def test_printing_options(self): + """ It should generate the right options dictionnary """ + self.assertEquals(self.Model.print_options('report', 'raw'), { + 'raw': 'True', + }) + self.assertEquals(self.Model.print_options('report', 'pdf', 2), { + 'copies': '2', + }) + self.assertEquals(self.Model.print_options('report', 'raw', 2), { + 'raw': 'True', + 'copies': '2', + }) + + @mock.patch('%s.cups' % server_model) + def test_print_report(self, cups): + """ It should print a report through CUPS """ + fd, file_name = tempfile.mkstemp() + with mock.patch('%s.mkstemp' % model) as mkstemp: + mkstemp.return_value = fd, file_name + printer = self.new_record() + printer.print_document('report_name', 'content to print', 'pdf') + cups.Connection().printFile.assert_called_once_with( + printer.system_name, + file_name, + file_name, + options={}) + + @mock.patch('%s.cups' % server_model) + def test_print_report_error(self, cups): + """ It should print a report through CUPS """ cups.Connection.side_effect = Exception - rec_id = self.new_record() - self.Model.update_printers_status() - self.assertEqual( - 'server-error', rec_id.status, - ) - - @mock.patch('%s.cups' % model) - def test_update_printers_status_inits_cups(self, cups): - """ It should init CUPS connection """ - self.new_record() - self.Model.update_printers_status() - cups.Connection.assert_called_once_with( - CUPS_HOST, CUPS_PORT, + fd, file_name = tempfile.mkstemp() + with mock.patch('%s.mkstemp' % model) as mkstemp: + mkstemp.return_value = fd, file_name + printer = self.new_record() + with self.assertRaises(UserError): + printer.print_document( + 'report_name', 'content to print', 'pdf') + + @mock.patch('%s.cups' % server_model) + def test_print_file(self, cups): + """ It should print a file through CUPS """ + file_name = 'file_name' + printer = self.new_record() + printer.print_file(file_name, 'pdf') + cups.Connection().printFile.assert_called_once_with( + printer.system_name, + file_name, + file_name, + options={}) + + @mock.patch('%s.cups' % server_model) + def test_print_file_error(self, cups): + """ It should print a file through CUPS """ + cups.Connection.side_effect = Exception + file_name = 'file_name' + printer = self.new_record() + with self.assertRaises(UserError): + printer.print_file(file_name) + + def test_set_default(self): + """ It should set a single record as default """ + printer = self.new_record() + self.assertTrue(printer.default) + other_printer = self.new_record() + other_printer.set_default() + self.assertFalse(printer.default) + self.assertTrue(other_printer.default) + # Check that calling the method on an empty recordset does nothing + self.Model.set_default() + self.assertEquals(other_printer, self.Model.get_default()) + + @mock.patch('%s.cups' % server_model) + def test_cancel_all_jobs(self, cups): + """ It should cancel all jobs """ + printer = self.new_record() + printer.action_cancel_all_jobs() + cups.Connection().cancelAllJobs.assert_called_once_with( + name=printer.system_name, + purge_jobs=False, ) - @mock.patch('%s.cups' % model) - def test_update_printers_status_gets_all_printers(self, cups): - """ It should get all printers from CUPS server """ - self.new_record() - self.Model.update_printers_status() - cups.Connection().getPrinters.assert_called_once_with() - - @mock.patch('%s.cups' % model) - def test_update_printers_status_gets_printer(self, cups): - """ It should get printer from CUPS by system_name """ - rec_id = self.new_record() - self.Model.update_printers_status() - cups.Connection().getPrinters().get.assert_called_once_with( - rec_id.system_name, + @mock.patch('%s.cups' % server_model) + def test_cancel_and_purge_all_jobs(self, cups): + """ It should cancel all jobs """ + printer = self.new_record() + printer.cancel_all_jobs(purge_jobs=True) + cups.Connection().cancelAllJobs.assert_called_once_with( + name=printer.system_name, + purge_jobs=True, ) - @mock.patch('%s.cups' % model) - def test_update_printers_status_search(self, cups): - """ It should search all when no domain """ - with mock.patch.object(self.Model, 'search') as search: - self.Model.update_printers_status() - search.assert_called_once_with([]) - - @mock.patch('%s.cups' % model) - def test_update_printers_status_search_domain(self, cups): - """ It should use specific domain for search """ - with mock.patch.object(self.Model, 'search') as search: - expect = [('id', '>', 0)] - self.Model.update_printers_status(expect) - search.assert_called_once_with(expect) - - @mock.patch('%s.cups' % model) - def test_update_printers_status_update_printer(self, cups): - """ It should update from CUPS when printer identified """ - with mock.patch.object(self.Model, 'search') as search: - printer_mk = mock.MagicMock() - search.return_value = [printer_mk] - self.Model.update_printers_status() - printer_mk.update_from_cups.assert_called_once_with( - cups.Connection(), - cups.Connection().getPrinters().get(), - ) - - @mock.patch('%s.cups' % model) - def test_update_printers_status_update_unavailable(self, cups): - """ It should update status when printer is unavailable """ - rec_id = self.new_record() - cups.Connection().getPrinters().get.return_value = False - self.Model.update_printers_status() - self.assertEqual( - 'unavailable', rec_id.status, - ) + @mock.patch('%s.cups' % server_model) + def test_enable_printer(self, cups): + """ It should enable the printer """ + printer = self.new_record() + printer.enable() + cups.Connection().enablePrinter.assert_called_once_with( + printer.system_name) + + @mock.patch('%s.cups' % server_model) + def test_disable_printer(self, cups): + """ It should disable the printer """ + printer = self.new_record() + printer.disable() + cups.Connection().disablePrinter.assert_called_once_with( + printer.system_name) diff --git a/base_report_to_printer/tests/test_printing_printer_wizard.py b/base_report_to_printer/tests/test_printing_printer_wizard.py index 2f7358cf18d..437e221dba9 100644 --- a/base_report_to_printer/tests/test_printing_printer_wizard.py +++ b/base_report_to_printer/tests/test_printing_printer_wizard.py @@ -4,17 +4,11 @@ import mock -from openerp.tests.common import TransactionCase -from openerp.exceptions import UserError +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError -from openerp.addons.base_report_to_printer.models.printing_printer import ( - CUPS_HOST, - CUPS_PORT, -) - -model = '%s.%s' % ('openerp.addons.base_report_to_printer.wizards', - 'printing_printer_update_wizard') +model = 'odoo.addons.base_report_to_printer.models.printing_server' class StopTest(Exception): @@ -26,6 +20,7 @@ class TestPrintingPrinterWizard(TransactionCase): def setUp(self): super(TestPrintingPrinterWizard, self).setUp() self.Model = self.env['printing.printer.update.wizard'] + self.server = self.env['printing.server'].create({}) self.printer_vals = { 'printer-info': 'Info', 'printer-make-and-model': 'Make and Model', @@ -36,6 +31,7 @@ def setUp(self): def _record_vals(self, sys_name='sys_name'): return { 'name': self.printer_vals['printer-info'], + 'server_id': self.server.id, 'system_name': sys_name, 'model': self.printer_vals['printer-make-and-model'], 'location': self.printer_vals['printer-location'], @@ -45,12 +41,9 @@ def _record_vals(self, sys_name='sys_name'): @mock.patch('%s.cups' % model) def test_action_ok_inits_connection(self, cups): """ It should initialize CUPS connection """ - try: - self.Model.action_ok() - except: - pass + self.Model.action_ok() cups.Connection.assert_called_once_with( - CUPS_HOST, CUPS_PORT, + host=self.server.address, port=self.server.port, ) @mock.patch('%s.cups' % model) @@ -83,8 +76,11 @@ def test_action_ok_creates_new_printer(self, cups): ) self.assertTrue(rec_id) for key, val in self._record_vals().iteritems(): + if rec_id._fields[key].type == 'many2one': + val = self.env[rec_id._fields[key].comodel_name].browse(val) + self.assertEqual( - val, getattr(rec_id, key), + val, rec_id[key], ) @mock.patch('%s.cups' % model) diff --git a/base_report_to_printer/tests/test_printing_report_xml_action.py b/base_report_to_printer/tests/test_printing_report_xml_action.py new file mode 100644 index 00000000000..52c83296eae --- /dev/null +++ b/base_report_to_printer/tests/test_printing_report_xml_action.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 SYLEAM +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.tests.common import TransactionCase + + +class TestPrintingReportXmlAction(TransactionCase): + + def setUp(self): + super(TestPrintingReportXmlAction, self).setUp() + self.Model = self.env['printing.report.xml.action'] + + self.report = self.env['ir.actions.report.xml'].search([], limit=1) + self.server = self.env['printing.server'].create({}) + + self.report_vals = { + 'report_id': self.report.id, + 'user_id': self.env.ref('base.user_demo').id, + 'action': 'server', + } + + def new_record(self, vals=None): + values = self.report_vals + if vals is not None: + values.update(vals) + + return self.Model.create(values) + + def new_printer(self): + return self.env['printing.printer'].create({ + 'name': 'Printer', + 'server_id': self.server.id, + 'system_name': 'Sys Name', + 'default': True, + 'status': 'unknown', + 'status_message': 'Msg', + 'model': 'res.users', + 'location': 'Location', + 'uri': 'URI', + }) + + def test_behaviour(self): + """ It should return some action's data, unless called on empty recordset + """ + xml_action = self.new_record() + self.assertEqual(xml_action.behaviour(), { + 'action': xml_action.action, + 'printer': xml_action.printer_id, + }) + + xml_action = self.new_record({'printer_id': self.new_printer().id}) + self.assertEqual(xml_action.behaviour(), { + 'action': xml_action.action, + 'printer': xml_action.printer_id, + }) + + self.assertEqual(self.Model.behaviour(), {}) diff --git a/base_report_to_printer/tests/test_printing_server.py b/base_report_to_printer/tests/test_printing_server.py new file mode 100644 index 00000000000..1bb8489fdcf --- /dev/null +++ b/base_report_to_printer/tests/test_printing_server.py @@ -0,0 +1,213 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import mock + +from odoo import fields +from odoo.tests.common import TransactionCase + + +model = 'odoo.addons.base_report_to_printer.models.printing_server' + + +class TestPrintingServer(TransactionCase): + + def setUp(self): + super(TestPrintingServer, self).setUp() + self.Model = self.env['printing.server'] + self.server = self.Model.create({}) + self.printer_vals = { + 'name': 'Printer', + 'server_id': self.server.id, + 'system_name': 'Sys Name', + 'default': True, + 'status': 'unknown', + 'status_message': 'Msg', + 'model': 'res.users', + 'location': 'Location', + 'uri': 'URI', + } + self.job_vals = { + 'server_id': self.server.id, + 'job_id_cups': 1, + 'job_media_progress': 0, + 'time_at_creation': fields.Datetime.now(), + } + + def new_printer(self): + return self.env['printing.printer'].create(self.printer_vals) + + def new_job(self, printer, vals=None): + values = self.job_vals + if vals is not None: + values.update(vals) + values['printer_id'] = printer.id + return self.env['printing.job'].create(values) + + @mock.patch('%s.cups' % model) + def test_update_printers_error(self, cups): + """ It should catch any exception from CUPS and update status """ + cups.Connection.side_effect = Exception + rec_id = self.new_printer() + self.Model.update_printers() + self.assertEqual( + 'server-error', rec_id.status, + ) + + @mock.patch('%s.cups' % model) + def test_update_printers_inits_cups(self, cups): + """ It should init CUPS connection """ + self.new_printer() + self.Model.update_printers() + cups.Connection.assert_called_once_with( + host=self.server.address, port=self.server.port, + ) + + @mock.patch('%s.cups' % model) + def test_update_printers_gets_all_printers(self, cups): + """ It should get all printers from CUPS server """ + self.new_printer() + self.Model.update_printers() + cups.Connection().getPrinters.assert_called_once_with() + + @mock.patch('%s.cups' % model) + def test_update_printers_search(self, cups): + """ It should search all when no domain """ + with mock.patch.object(self.Model, 'search') as search: + self.Model.update_printers() + search.assert_called_once_with([]) + + @mock.patch('%s.cups' % model) + def test_update_printers_search_domain(self, cups): + """ It should use specific domain for search """ + with mock.patch.object(self.Model, 'search') as search: + expect = [('id', '>', 0)] + self.Model.update_printers(expect) + search.assert_called_once_with(expect) + + @mock.patch('%s.cups' % model) + def test_update_printers_update_unavailable(self, cups): + """ It should update status when printer is unavailable """ + rec_id = self.new_printer() + cups.Connection().getPrinters().get.return_value = False + self.Model.action_update_printers() + self.assertEqual( + 'unavailable', rec_id.status, + ) + + @mock.patch('%s.cups' % model) + def test_update_jobs_cron(self, cups): + """ It should get all jobs from CUPS server """ + self.new_printer() + self.Model.action_update_jobs() + cups.Connection().getPrinters.assert_called_once_with() + cups.Connection().getJobs.assert_called_once_with( + which_jobs='all', + first_job_id=-1, + requested_attributes=[ + 'job-name', + 'job-id', + 'printer-uri', + 'job-media-progress', + 'time-at-creation', + 'job-state', + 'job-state-reasons', + 'time-at-processing', + 'time-at-completed', + ], + ) + + @mock.patch('%s.cups' % model) + def test_update_jobs_button(self, cups): + """ It should get all jobs from CUPS server """ + self.new_printer() + self.server.action_update_jobs() + cups.Connection().getPrinters.assert_called_once_with() + cups.Connection().getJobs.assert_called_once_with( + which_jobs='all', + first_job_id=-1, + requested_attributes=[ + 'job-name', + 'job-id', + 'printer-uri', + 'job-media-progress', + 'time-at-creation', + 'job-state', + 'job-state-reasons', + 'time-at-processing', + 'time-at-completed', + ], + ) + + @mock.patch('%s.cups' % model) + def test_update_jobs_error(self, cups): + """ It should catch any exception from CUPS and update status """ + cups.Connection.side_effect = Exception + self.new_printer() + self.server.update_jobs() + cups.Connection.assert_called_with( + host=self.server.address, port=self.server.port, + ) + + @mock.patch('%s.cups' % model) + def test_update_jobs_uncompleted(self, cups): + """ + It should search which jobs have been completed since last update + """ + printer = self.new_printer() + self.new_job(printer, vals={'job_state': 'completed'}) + self.new_job(printer, vals={ + 'job_id_cups': 2, + 'job_state': 'processing', + }) + self.server.update_jobs(which='not-completed') + cups.Connection().getJobs.assert_any_call( + which_jobs='completed', first_job_id=2, + requested_attributes=[ + 'job-name', + 'job-id', + 'printer-uri', + 'job-media-progress', + 'time-at-creation', + 'job-state', + 'job-state-reasons', + 'time-at-processing', + 'time-at-completed', + ], + ) + + @mock.patch('%s.cups' % model) + def test_update_jobs(self, cups): + """ + It should update all jobs, known or not + """ + printer = self.new_printer() + printer_uri = 'hostname:port/' + printer.system_name + cups.Connection().getJobs.return_value = { + 1: { + 'printer-uri': printer_uri, + }, + 2: { + 'printer-uri': printer_uri, + 'job-state': 9, + }, + 4: { + 'printer-uri': printer_uri, + 'job-state': 5, + }, + } + self.new_job(printer, vals={'job_state': 'completed'}) + completed_job = self.new_job(printer, vals={ + 'job_id_cups': 2, + 'job_state': 'processing', + }) + purged_job = self.new_job(printer, vals={ + 'job_id_cups': 3, + 'job_state': 'processing', + }) + self.server.update_jobs() + new_job = self.env['printing.job'].search([('job_id_cups', '=', 4)]) + self.assertEqual(completed_job.job_state, 'completed') + self.assertEqual(purged_job.active, False) + self.assertEqual(new_job.job_state, 'processing') diff --git a/base_report_to_printer/tests/test_report.py b/base_report_to_printer/tests/test_report.py index 2f7f272109d..fca7a1a5c1e 100644 --- a/base_report_to_printer/tests/test_report.py +++ b/base_report_to_printer/tests/test_report.py @@ -2,7 +2,9 @@ # Copyright 2016 LasLabs Inc. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp.tests.common import TransactionCase +import mock +from odoo.tests.common import TransactionCase +from odoo import exceptions class StopTest(Exception): @@ -14,11 +16,25 @@ class TestReport(TransactionCase): def setUp(self): super(TestReport, self).setUp() self.Model = self.env['report'] + self.server = self.env['printing.server'].create({}) self.report_vals = {} def new_record(self): return self.Model.create(self.report_vals) + def new_printer(self): + return self.env['printing.printer'].create({ + 'name': 'Printer', + 'server_id': self.server.id, + 'system_name': 'Sys Name', + 'default': True, + 'status': 'unknown', + 'status_message': 'Msg', + 'model': 'res.users', + 'location': 'Location', + 'uri': 'URI', + }) + def test_can_print_report_context_skip(self): """ It should return False based on context """ rec_id = self.new_record().with_context( @@ -42,3 +58,72 @@ def test_can_print_report_false(self): {'action': 'server'}, True, False ) self.assertFalse(res) + + def test_get_pdf_not_printable(self): + """ It should print the report, only if it is printable + """ + with mock.patch('odoo.addons.base_report_to_printer.models.' + 'printing_printer.PrintingPrinter.' + 'print_document') as print_document: + report = self.env['ir.actions.report.xml'].search([ + ('report_type', '=', 'qweb-pdf'), + ], limit=1) + records = self.env[report.model].search([], limit=5) + self.env['report'].get_pdf(records.ids, report.report_name) + print_document.assert_not_called() + + def test_get_pdf_printable(self): + """ It should print the report, only if it is printable + """ + with mock.patch('odoo.addons.base_report_to_printer.models.' + 'printing_printer.PrintingPrinter.' + 'print_document') as print_document: + report = self.env['ir.actions.report.xml'].search([ + ('report_type', '=', 'qweb-pdf'), + ], limit=1) + report.property_printing_action_id.action_type = 'server' + report.printing_printer_id = self.new_printer() + records = self.env[report.model].search([], limit=5) + document = self.env['report'].get_pdf( + records.ids, report.report_name) + print_document.assert_called_once_with( + report, document, report.report_type) + + def test_print_document_not_printable(self): + """ It should print the report, regardless of the defined behaviour """ + report = self.env['ir.actions.report.xml'].search([ + ('report_type', '=', 'qweb-pdf'), + ], limit=1) + report.printing_printer_id = self.new_printer() + records = self.env[report.model].search([], limit=5) + + with mock.patch('odoo.addons.base_report_to_printer.models.' + 'printing_printer.PrintingPrinter.' + 'print_document') as print_document: + self.env['report'].print_document(records.ids, report.report_name) + print_document.assert_called_once() + + def test_print_document_printable(self): + """ It should print the report, regardless of the defined behaviour """ + report = self.env['ir.actions.report.xml'].search([ + ('report_type', '=', 'qweb-pdf'), + ], limit=1) + report.property_printing_action_id.action_type = 'server' + report.printing_printer_id = self.new_printer() + records = self.env[report.model].search([], limit=5) + + with mock.patch('odoo.addons.base_report_to_printer.models.' + 'printing_printer.PrintingPrinter.' + 'print_document') as print_document: + self.env['report'].print_document(records.ids, report.report_name) + print_document.assert_called_once() + + def test_print_document_no_printer(self): + """ It should raise an error """ + report = self.env['ir.actions.report.xml'].search([ + ('report_type', '=', 'qweb-pdf'), + ], limit=1) + records = self.env[report.model].search([], limit=5) + + with self.assertRaises(exceptions.UserError): + self.env['report'].print_document(records.ids, report.report_name) diff --git a/base_report_to_printer/tests/test_res_users.py b/base_report_to_printer/tests/test_res_users.py index 3db81652e3c..f02a4c89de9 100644 --- a/base_report_to_printer/tests/test_res_users.py +++ b/base_report_to_printer/tests/test_res_users.py @@ -2,7 +2,7 @@ # Copyright 2016 LasLabs Inc. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp.tests.common import TransactionCase +from odoo.tests.common import TransactionCase class TestResUsers(TransactionCase): diff --git a/base_report_to_printer/views/printing_job.xml b/base_report_to_printer/views/printing_job.xml new file mode 100644 index 00000000000..856596c2f6f --- /dev/null +++ b/base_report_to_printer/views/printing_job.xml @@ -0,0 +1,44 @@ + + + + + printing.job.form + printing.job + +
+ +
+
+ + + + + + + + + + + + + +
+
+
+
+ + + printing.job.tree + printing.job + + + + + + + + + +
diff --git a/base_report_to_printer/views/printing_printer_view.xml b/base_report_to_printer/views/printing_printer_view.xml index 73b84c80dfa..864505f7ebd 100644 --- a/base_report_to_printer/views/printing_printer_view.xml +++ b/base_report_to_printer/views/printing_printer_view.xml @@ -1,81 +1,90 @@ - + - - printing.printer.form - printing.printer - -
- -
-

-

-
- - - - - -