From 183a8ee45a2e17a69123d13041d1e825f7d3c408 Mon Sep 17 00:00:00 2001 From: Laurent Mignon Date: Sat, 17 Dec 2016 10:07:47 +0100 Subject: [PATCH] [IMP] Replace old style parser by TransientModel The goal is to improve the modularity by making the parser a true inheritable odoo model and share part of the code with the 'report' model Conflicts: report_py3o/models/ir_actions_report_xml.py report_py3o/models/py3o_report.py report_py3o/tests/test_report_py3o.py --- report_py3o/models/__init__.py | 1 + report_py3o/models/ir_actions_report_xml.py | 53 +---- .../{py3o_parser.py => models/py3o_report.py} | 189 ++++++++++++------ report_py3o/tests/test_report_py3o.py | 9 +- 4 files changed, 148 insertions(+), 104 deletions(-) rename report_py3o/{py3o_parser.py => models/py3o_report.py} (52%) diff --git a/report_py3o/models/__init__.py b/report_py3o/models/__init__.py index a8e7b0a6..425cb3d9 100644 --- a/report_py3o/models/__init__.py +++ b/report_py3o/models/__init__.py @@ -1,3 +1,4 @@ from . import ir_actions_report_xml from . import py3o_template from . import py3o_server +from . import py3o_report diff --git a/report_py3o/models/ir_actions_report_xml.py b/report_py3o/models/ir_actions_report_xml.py index 3a290f1d..bbd95923 100644 --- a/report_py3o/models/ir_actions_report_xml.py +++ b/report_py3o/models/ir_actions_report_xml.py @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- # Copyright 2013 XCG Consulting (http://odoo.consulting) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -import os import logging from odoo import api, fields, models, _ from odoo.report.interface import report_int from odoo.exceptions import ValidationError from odoo import addons -from ..py3o_parser import Py3oParser logger = logging.getLogger(__name__) @@ -85,43 +83,14 @@ class IrActionsReportXml(models.Model): )) report_type = fields.Selection(selection_add=[('py3o', "Py3o")]) - @api.model_cr - def _lookup_report(self, name): - """Look up a report definition. - """ - # START section copied from odoo/addons/base/ir/ir_actions.py - # with small adaptations - # First lookup in the deprecated place, because if the report - # definition has not been updated, it is more likely the correct - # definition is there. Only reports with custom parser - # specified in Python are still there. - if 'report.' + name in report_int._reports: - new_report = report_int._reports['report.' + name] - if not isinstance(new_report, Py3oParser): - new_report = None - else: - self._cr.execute( - "SELECT * FROM ir_act_report_xml " - "WHERE report_name=%s AND report_type=%s", (name, 'py3o')) - report_data = self._cr.dictfetchone() - # END section copied from odoo/addons/base/ir/ir_actions.py - if report_data: - kwargs = {} - if report_data['parser']: - kwargs['parser'] = getattr(addons, report_data['parser']) - - new_report = Py3oParser( - 'report.' + report_data['report_name'], - report_data['model'], - os.path.join('addons', report_data['report_rml'] or '/'), - header=report_data['header'], - register=False, - **kwargs - ) - else: - new_report = None - - if new_report: - return new_report - else: - return super(IrActionsReportXml, self)._lookup_report(name) + @api.model + def render_report(self, res_ids, name, data): + action_py3o_report = self.search( + [("report_name", "=", name), + ("report_type", "=", "py3o")]) + if action_py3o_report: + return self.env['py3o.report'].create({ + 'ir_actions_report_xml_id': action_py3o_report.id + }).create_report(res_ids, data) + return super(IrActionsReportXml, self).render_report( + res_ids, name, data) diff --git a/report_py3o/py3o_parser.py b/report_py3o/models/py3o_report.py similarity index 52% rename from report_py3o/py3o_parser.py rename to report_py3o/models/py3o_report.py index 44e856c0..7c479455 100644 --- a/report_py3o/py3o_parser.py +++ b/report_py3o/models/py3o_report.py @@ -1,24 +1,29 @@ # -*- coding: utf-8 -*- # Copyright 2013 XCG Consulting (http://odoo.consulting) +# Copyright 2016 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +import base64 +from base64 import b64decode from cStringIO import StringIO import json -import pkg_resources +import logging import os -import sys -from base64 import b64decode +import pkg_resources import requests +import sys from tempfile import NamedTemporaryFile -from odoo import api, _ from odoo import exceptions from odoo.report.report_sxw import report_sxw import logging +from zipfile import ZipFile, ZIP_DEFLATED +from openerp import api, fields, models, _ logger = logging.getLogger(__name__) try: from py3o.template.helpers import Py3oConvertor from py3o.template import Template + from py3o import formats except ImportError: logger.debug('Cannot import py3o.template') try: @@ -64,11 +69,18 @@ def defautl_extend(report_xml, localcontext): localcontext['report_xml'] = report_xml -class Py3oParser(report_sxw): - """Custom class that use Py3o to render libroffice reports. - Code partially taken from CampToCamp's webkit_report.""" +class Py3oReport(models.TransientModel): + _name = "py3o.report" + _inherit = 'report' + _description = "Report Py30" + + ir_actions_report_xml_id = fields.Many2one( + comodel_name="ir.actions.report.xml", + required=True + ) - def get_template(self, report_obj): + @api.multi + def get_template(self): """private helper to fetch the template data either from the database or from the default template file provided by the implementer. @@ -76,30 +88,27 @@ class Py3oParser(report_sxw): to try and fetch the report template from database. If not found it will fallback to the template file referenced in the report definition. - @param report_obj: a recordset representing the report defintion - @type report_obj: odoo.model.recordset instance - @returns: string or buffer containing the template data @raises: TemplateNotFound which is a subclass of odoo.exceptions.DeferredException """ - + self.ensure_one() tmpl_data = None - - if report_obj.py3o_template_id and report_obj.py3o_template_id.id: + report_xml = self.ir_actions_report_xml_id + if report_xml.py3o_template_id and report_xml.py3o_template_id.id: # if a user gave a report template tmpl_data = b64decode( - report_obj.py3o_template_id.py3o_template_data + report_xml.py3o_template_id.py3o_template_data ) - elif report_obj.py3o_template_fallback: - tmpl_name = report_obj.py3o_template_fallback + elif report_xml.py3o_template_fallback: + tmpl_name = report_xml.py3o_template_fallback flbk_filename = None - if report_obj.module: + if report_xml.module: # if the default is defined flbk_filename = pkg_resources.resource_filename( - "odoo.addons.%s" % report_obj.module, + "odoo.addons.%s" % report_xml.module, tmpl_name, ) elif os.path.isabs(tmpl_name): @@ -119,37 +128,54 @@ class Py3oParser(report_sxw): return tmpl_data - def _extend_parser_context(self, parser_instance, report_xml): + @api.multi + def _extend_parser_context(self, context_instance, report_xml): # add default extenders for fct in _extender_functions.get(None, []): - fct(report_xml, parser_instance.localcontext) + fct(report_xml, context_instance.localcontext) # add extenders for registered on the template xml_id = report_xml.get_external_id().get(report_xml.id) if xml_id in _extender_functions: for fct in _extender_functions[xml_id]: - fct(report_xml, parser_instance.localcontext) + fct(report_xml, context_instance.localcontext) + + @api.multi + def _get_parser_context(self, model_instance, data): + report_xml = self.ir_actions_report_xml_id + context_instance = rml_parse(self.env.cr, self.env.uid, + report_xml.name, + context=self.env.context) + context_instance.set_context(model_instance, data, model_instance.ids, + report_xml.report_type) + self._extend_parser_context(context_instance, report_xml) + return context_instance.localcontext + + @api.multi + def _postprocess_report(self, content, res_id, save_in_attachment): + if save_in_attachment.get(res_id): + attachment = { + 'name': save_in_attachment.get(res_id), + 'datas': base64.encodestring(content), + 'datas_fname': save_in_attachment.get(res_id), + 'res_model': save_in_attachment.get('model'), + 'res_id': res_id, + } + return self.env['ir.attachment'].create(attachment) + return False - def create_single_pdf(self, cr, uid, ids, data, report_xml, context=None): - """ Overide this function to generate our py3o report + @api.multi + def _create_single_report(self, model_instance, data, save_in_attachment): + """ This function to generate our py3o report """ - if report_xml.report_type != 'py3o': - return super(Py3oParser, self).create_single_pdf( - cr, uid, ids, data, report_xml, context=context - ) - - parser_instance = self.parser(cr, uid, self.name2, context=context) - parser_instance.set_context( - self.getObjects(cr, uid, ids, context), - data, ids, report_xml.report_type - ) - self._extend_parser_context(parser_instance, report_xml) + self.ensure_one() + report_xml = self.ir_actions_report_xml_id - tmpl_data = self.get_template(report_xml) + tmpl_data = self.get_template() in_stream = StringIO(tmpl_data) out_stream = StringIO() template = Template(in_stream, out_stream, escape_false=True) - localcontext = parser_instance.localcontext + localcontext = self._get_parser_context(model_instance, data) if report_xml.py3o_is_local_fusion: template.render(localcontext) in_stream = out_stream @@ -181,7 +207,7 @@ class Py3oParser(report_sxw): report_xml.py3o_server_id.url, data=fields, files=files) if r.status_code != 200: # server says we have an issue... let's tell that to enduser - raise exceptions.Warning( + raise UserError( _('Fusion server error %s') % r.text, ) @@ -189,32 +215,79 @@ class Py3oParser(report_sxw): # we do nice chunked reading from the network... chunk_size = 1024 with NamedTemporaryFile( - suffix=filetype, - prefix='py3o-template-' + suffix=filetype, + prefix='py3o-template-' ) as fd: for chunk in r.iter_content(chunk_size): fd.write(chunk) fd.seek(0) # ... but odoo wants the whole data in memory anyways :) res = fd.read() + self._postprocess_report( + res, model_instance.id, save_in_attachment) + return res, "." + self.ir_actions_report_xml_id.py3o_filetype + + @api.multi + def _get_or_create_single_report(self, model_instance, data, + save_in_attachment): + self.ensure_one() + if save_in_attachment and save_in_attachment[ + 'loaded_documents'].get(model_instance.id): + d = save_in_attachment[ + 'loaded_documents'].get(model_instance.id) + return d, self.ir_actions_report_xml_id.py3o_filetype + return self._create_single_report( + model_instance, data, save_in_attachment) + + @api.multi + def _zip_results(self, results): + self.ensure_one() + zfname_prefix = self.ir_actions_report_xml_id.name + with NamedTemporaryFile(suffix="zip", prefix='py3o-zip-result') as fd: + with ZipFile(fd, 'w', ZIP_DEFLATED) as zf: + cpt = 0 + for r, ext in results: + fname = "%s_%d.%s" % (zfname_prefix, cpt, ext) + zf.writestr(fname, r) + cpt += 1 + fd.seek(0) + return fd.read(), 'zip' + + @api.multi + def _merge_pdfs(self, results): + from pyPdf import PdfFileWriter, PdfFileReader + output = PdfFileWriter() + for r in results: + reader = PdfFileReader(StringIO(r[0])) + for page in range(reader.getNumPages()): + output.addPage(reader.getPage(page)) + s = StringIO() + output.write(s) + return s.getvalue(), formats.FORMAT_PDF + + @api.multi + def _merge_results(self, results): + self.ensure_one() + if not results: + return False, False + if len(results) == 1: + return results[0] + filetype = self.ir_actions_report_xml_id.py3o_filetype + if filetype == formats.FORMAT_PDF: + return self._merge_pdfs(results) + else: + return self._zip_results(results) - return res, filetype - - def create(self, cr, uid, ids, data, context=None): + @api.multi + def create_report(self, res_ids, data): """ Override this function to handle our py3o report """ - env = api.Environment(cr, uid, context) - report_xmls = env['ir.actions.report.xml'].search( - [('report_name', '=', self.name[7:])]) - if not report_xmls: - return super(Py3oParser, self).create( - cr, uid, ids, data, context=context - ) - - result = self.create_source_pdf( - cr, uid, ids, data, report_xmls[0], context - ) - - if not result: - return False, False - return result + model_instances = self.env[self.ir_actions_report_xml_id.model].browse( + res_ids) + save_in_attachment = self._check_attachment_use( + model_instances, self.ir_actions_report_xml_id) or {} + results = [] + for model_instance in model_instances: + results.append(self._get_or_create_single_report( + model_instance, data, save_in_attachment)) + return self._merge_results(results) diff --git a/report_py3o/tests/test_report_py3o.py b/report_py3o/tests/test_report_py3o.py index 18438c34..780087b6 100644 --- a/report_py3o/tests/test_report_py3o.py +++ b/report_py3o/tests/test_report_py3o.py @@ -11,7 +11,7 @@ from py3o.formats import Formats from odoo.tests.common import TransactionCase from odoo.exceptions import ValidationError -from ..py3o_parser import TemplateNotFound +from ..models.py3o_report import TemplateNotFound from base64 import b64encode @@ -56,9 +56,10 @@ class TestReportPy3o(TransactionCase): "Field 'Output Format' is required for Py3O report") def test_reports(self): + py3o_report = self.env['py3o.report'] report = self.env.ref("report_py3o.res_users_report_py3o") - with mock.patch('odoo.addons.report_py3o.py3o_parser.' - 'Py3oParser.create_single_pdf') as patched_pdf: + with mock.patch.object( + py3o_report.__class__, '_create_single_report') as patched_pdf: # test the call the the create method inside our custom parser report.render_report(self.env.user.ids, report.report_name, @@ -98,7 +99,7 @@ class TestReportPy3o(TransactionCase): report.render_report( self.env.user.ids, report.report_name, {}) - # the template can also be provivided as an abspaath + # the template can also be provided as an abspaath report.py3o_template_fallback = flbk_filename res = report.render_report( self.env.user.ids, report.report_name, {})