[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
This commit is contained in:
Sylvain Garancher
2017-04-05 18:06:16 +02:00
committed by Carlos Roca
parent 6c943e09b8
commit 5dda3b475e
32 changed files with 1482 additions and 417 deletions

View File

@@ -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

View File

@@ -6,7 +6,7 @@
# Copyright (C) 2013-2014 Camptocamp (<http://www.camptocamp.com>)
# 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 @@ class IrActionsReportXml(models.Model):
# 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

View File

@@ -6,23 +6,25 @@
# Copyright (C) 2013-2014 Camptocamp (<http://www.camptocamp.com>)
# 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'
)

View File

@@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 SYLEAM (<http://www.syleam.fr>)
# 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

View File

@@ -4,6 +4,7 @@
# Copyright (C) 2011 Agile Business Group sagl (<http://www.agilebg.com>)
# Copyright (C) 2011 Domsense srl (<http://www.domsense.com>)
# Copyright (C) 2013-2014 Camptocamp (<http://www.camptocamp.com>)
# Copyright (C) 2016 SYLEAM (<http://www.syleam.fr>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
@@ -11,24 +12,12 @@ import logging
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 @@ class PrintingPrinter(models.Model):
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 @@ class PrintingPrinter(models.Model):
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))
return self.print_file(file_name, report=report, copies=copies)
options = self.print_options(report, format, 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 @@ class PrintingPrinter(models.Model):
@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

View File

@@ -6,9 +6,7 @@
# Copyright (C) 2013-2014 Camptocamp (<http://www.camptocamp.com>)
# 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,
}

View File

@@ -0,0 +1,224 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 SYLEAM (<http://www.syleam.fr>)
# 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

View File

@@ -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 @@ class Report(models.Model):
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

View File

@@ -6,26 +6,23 @@
# Copyright (C) 2013-2014 Camptocamp (<http://www.camptocamp.com>)
# 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']