From 6c74f0ed2665114feff5c1d44711045c7010ade9 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Wed, 16 Nov 2016 14:57:14 +0100 Subject: [PATCH 1/6] [IMP] Escape 'False' when rendering value --- report_py3o/py3o_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/report_py3o/py3o_parser.py b/report_py3o/py3o_parser.py index 4fc1e90d..44e856c0 100644 --- a/report_py3o/py3o_parser.py +++ b/report_py3o/py3o_parser.py @@ -148,7 +148,7 @@ class Py3oParser(report_sxw): in_stream = StringIO(tmpl_data) out_stream = StringIO() - template = Template(in_stream, out_stream) + template = Template(in_stream, out_stream, escape_false=True) localcontext = parser_instance.localcontext if report_xml.py3o_is_local_fusion: template.render(localcontext) From ac98e1bbc6e004a72e0219d0f175a06ed8c5df6c Mon Sep 17 00:00:00 2001 From: Laurent Mignon Date: Sat, 17 Dec 2016 10:07:47 +0100 Subject: [PATCH 2/6] [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, {}) From efcbd1d3ad1f57ecb302c267716f8ed3f6822746 Mon Sep 17 00:00:00 2001 From: "Jonathan Nemry (ACSONE)" Date: Thu, 22 Dec 2016 18:07:46 +0100 Subject: [PATCH 3/6] [FIX] imports --- report_py3o/models/py3o_report.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/report_py3o/models/py3o_report.py b/report_py3o/models/py3o_report.py index 7c479455..11dabcd9 100644 --- a/report_py3o/models/py3o_report.py +++ b/report_py3o/models/py3o_report.py @@ -12,11 +12,12 @@ import pkg_resources import requests import sys from tempfile import NamedTemporaryFile -from odoo import exceptions -from odoo.report.report_sxw import report_sxw import logging from zipfile import ZipFile, ZIP_DEFLATED + +from odoo.exceptions import UserError from openerp import api, fields, models, _ +from odoo.report.report_sxw import rml_parse logger = logging.getLogger(__name__) From f592b753ec84f99967ecc188c999478fb4aa7b07 Mon Sep 17 00:00:00 2001 From: Laurent Mignon Date: Fri, 23 Dec 2016 12:43:57 +0100 Subject: [PATCH 4/6] [IMP] Minimizes memory consumption Conflicts: report_py3o/models/py3o_report.py --- report_py3o/models/py3o_report.py | 185 +++++++++++++++----------- report_py3o/tests/test_report_py3o.py | 7 + 2 files changed, 114 insertions(+), 78 deletions(-) diff --git a/report_py3o/models/py3o_report.py b/report_py3o/models/py3o_report.py index 11dabcd9..b52bffbd 100644 --- a/report_py3o/models/py3o_report.py +++ b/report_py3o/models/py3o_report.py @@ -8,16 +8,18 @@ from cStringIO import StringIO import json import logging import os +from contextlib import closing + import pkg_resources import requests import sys -from tempfile import NamedTemporaryFile -import logging +import tempfile from zipfile import ZipFile, ZIP_DEFLATED +from odoo.exceptions import AccessError from odoo.exceptions import UserError -from openerp import api, fields, models, _ from odoo.report.report_sxw import rml_parse +from odoo import api, fields, models, _ logger = logging.getLogger(__name__) @@ -151,18 +153,40 @@ class Py3oReport(models.TransientModel): self._extend_parser_context(context_instance, report_xml) return context_instance.localcontext - @api.multi - def _postprocess_report(self, content, res_id, save_in_attachment): + @api.model + def _get_report_from_name(self, report_name): + """Get the first record of ir.actions.report.xml having the + ``report_name`` as value for the field report_name. + """ + res = super(Py3oReport, self)._get_report_from_name(report_name) + if res: + return res + # maybe a py3o reprot + report_obj = self.env['ir.actions.report.xml'] + return report_obj.search( + [('report_type', '=', 'py3o'), + ('report_name', '=', report_name)]) + + @api.model + def _postprocess_report(self, report_path, 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 + with open(report_path, 'rb') as pdfreport: + attachment = { + 'name': save_in_attachment.get(res_id), + 'datas': base64.encodestring(pdfreport.read()), + 'datas_fname': save_in_attachment.get(res_id), + 'res_model': save_in_attachment.get('model'), + 'res_id': res_id, + } + try: + self.env['ir.attachment'].create(attachment) + except AccessError: + logger.info("Cannot save PDF report %r as attachment", + attachment['name']) + else: + logger.info( + 'The PDF document %s is now saved in the database', + attachment['name']) @api.multi def _create_single_report(self, model_instance, data, save_in_attachment): @@ -170,30 +194,31 @@ class Py3oReport(models.TransientModel): """ self.ensure_one() report_xml = self.ir_actions_report_xml_id - + filetype = report_xml.py3o_filetype + result_fd, result_path = tempfile.mkstemp( + suffix='.' + filetype, prefix='p3o.report.tmp.') tmpl_data = self.get_template() in_stream = StringIO(tmpl_data) - out_stream = StringIO() - template = Template(in_stream, out_stream, escape_false=True) - localcontext = self._get_parser_context(model_instance, data) - if report_xml.py3o_is_local_fusion: - template.render(localcontext) - in_stream = out_stream - datadict = {} - else: - expressions = template.get_all_user_python_expression() - py_expression = template.convert_py3o_to_python_ast(expressions) - convertor = Py3oConvertor() - data_struct = convertor(py_expression) - datadict = data_struct.render(localcontext) - - filetype = report_xml.py3o_filetype - is_native = Formats().get_format(filetype).native - if is_native: - res = out_stream.getvalue() - else: # Call py3o.server to render the template in the desired format - in_stream.seek(0) + with closing(os.fdopen(result_fd, 'w+')) as out_stream: + template = Template(in_stream, out_stream, escape_false=True) + localcontext = self._get_parser_context(model_instance, data) + is_native = Formats().get_format(filetype).native + if report_xml.py3o_is_local_fusion: + template.render(localcontext) + out_stream.seek(0) + in_stream = out_stream.read() + datadict = {} + else: + expressions = template.get_all_user_python_expression() + py_expression = template.convert_py3o_to_python_ast( + expressions) + convertor = Py3oConvertor() + data_struct = convertor(py_expression) + datadict = data_struct.render(localcontext) + + if not is_native or not report_xml.py3o_is_local_fusion: + # Call py3o.server to render the template in the desired format files = { 'tmpl_file': in_stream, } @@ -212,21 +237,13 @@ class Py3oReport(models.TransientModel): _('Fusion server error %s') % r.text, ) - # Here is a little joke about Odoo - # we do nice chunked reading from the network... chunk_size = 1024 - with NamedTemporaryFile( - suffix=filetype, - prefix='py3o-template-' - ) as fd: + with open(result_path, 'w+') 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 + result_path, model_instance.id, save_in_attachment) + return result_path @api.multi def _get_or_create_single_report(self, model_instance, data, @@ -241,43 +258,42 @@ class Py3oReport(models.TransientModel): model_instance, data, save_in_attachment) @api.multi - def _zip_results(self, results): + def _zip_results(self, reports_path): 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' + result_path = tempfile.mktemp(suffix="zip", prefix='py3o-zip-result') + with ZipFile(result_path, 'w', ZIP_DEFLATED) as zf: + cpt = 0 + for report in reports_path: + fname = "%s_%d.%s" % ( + zfname_prefix, cpt, report.split('.')[-1]) + zf.write(report, fname) - @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 + cpt += 1 + return result_path @api.multi - def _merge_results(self, results): + def _merge_results(self, reports_path): 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 not reports_path: + return False, False + if len(reports_path) == 1: + return reports_path[0], filetype if filetype == formats.FORMAT_PDF: - return self._merge_pdfs(results) + return self._merge_pdf(reports_path), formats.FORMAT_PDF else: - return self._zip_results(results) + return self._zip_results(reports_path), 'zip' + + @api.model + def _cleanup_tempfiles(self, temporary_files): + # Manual cleanup of the temporary files + for temporary_file in temporary_files: + try: + os.unlink(temporary_file) + except (OSError, IOError): + logger.error( + 'Error when trying to remove file %s' % temporary_file) @api.multi def create_report(self, res_ids, data): @@ -287,8 +303,21 @@ class Py3oReport(models.TransientModel): res_ids) save_in_attachment = self._check_attachment_use( model_instances, self.ir_actions_report_xml_id) or {} - results = [] + reports_path = [] 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) + reports_path.append( + self._get_or_create_single_report( + model_instance, data, save_in_attachment)) + + result_path, filetype = self._merge_results(reports_path) + reports_path.append(result_path) + + # Here is a little joke about Odoo + # we do all the generation process using files to avoid memory + # consumption... + # ... but odoo wants the whole data in memory anyways :) + + with open(result_path, 'r+b') as fd: + res = fd.read() + self._cleanup_tempfiles(set(reports_path)) + return res, filetype diff --git a/report_py3o/tests/test_report_py3o.py b/report_py3o/tests/test_report_py3o.py index 780087b6..70e4a513 100644 --- a/report_py3o/tests/test_report_py3o.py +++ b/report_py3o/tests/test_report_py3o.py @@ -5,6 +5,7 @@ import mock import os import pkg_resources +import tempfile from py3o.formats import Formats @@ -60,11 +61,17 @@ class TestReportPy3o(TransactionCase): report = self.env.ref("report_py3o.res_users_report_py3o") with mock.patch.object( py3o_report.__class__, '_create_single_report') as patched_pdf: + result = tempfile.mktemp('.txt') + with open(result, 'w') as fp: + fp.write('dummy') + patched_pdf.return_value = result # test the call the the create method inside our custom parser report.render_report(self.env.user.ids, report.report_name, {}) self.assertEqual(1, patched_pdf.call_count) + # generated files no more exists + self.assertFalse(os.path.exists(result)) res = report.render_report( self.env.user.ids, report.report_name, {}) self.assertTrue(res) From 7bda0cb92a0a0c9f1e4ba93177d205f43ebac03f Mon Sep 17 00:00:00 2001 From: "Jonathan Nemry (ACSONE)" Date: Mon, 9 Jan 2017 15:28:22 +0100 Subject: [PATCH 5/6] * travis.yml * flake8 --- .travis.yml | 2 +- report_py3o/models/ir_actions_report_xml.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 75e8bc4c..fd7bbfc6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ python: env: global: - - VERSION="9.0" TESTS="0" LINT_CHECK="0" TRANSIFEX="0" + - VERSION="10.0" TESTS="0" LINT_CHECK="0" TRANSIFEX="0" - TRANSIFEX_USER='transbot@odoo-community.org' - secure: NUsXwVrMntcqge1ozKW+DSkP7dq+Rla6JVvFF2c89/g+zJaIqQRi8EQBLoqNwCdMk+rjpQeZt/JPELjH+EzPcmGddhDxOgVB3nUT9LvFXGCHF+NjmHXqyba4tuc7BnpG1WDD+rSlxVCt1aIjNIhhaZ4ic0rCWpKNYu/yFTsmChc= matrix: diff --git a/report_py3o/models/ir_actions_report_xml.py b/report_py3o/models/ir_actions_report_xml.py index bbd95923..cfbfeb41 100644 --- a/report_py3o/models/ir_actions_report_xml.py +++ b/report_py3o/models/ir_actions_report_xml.py @@ -3,9 +3,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging from odoo import api, fields, models, _ -from odoo.report.interface import report_int from odoo.exceptions import ValidationError -from odoo import addons logger = logging.getLogger(__name__) From 4048c0b8c134f6ac55a8779803b4fc21fe4f5339 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Wed, 25 Jan 2017 11:56:21 +0100 Subject: [PATCH 6/6] [IMP] Allow to override/extend the way we get the fallback template --- report_py3o/models/py3o_report.py | 57 ++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/report_py3o/models/py3o_report.py b/report_py3o/models/py3o_report.py index b52bffbd..ccebbcb8 100644 --- a/report_py3o/models/py3o_report.py +++ b/report_py3o/models/py3o_report.py @@ -83,7 +83,41 @@ class Py3oReport(models.TransientModel): ) @api.multi - def get_template(self): + def _get_template_from_path(self, tmpl_name): + """"Return the template from the path to root of the module if specied + or an absolute path on your server + """ + if not tmpl_name: + return None + report_xml = self.ir_actions_report_xml_id + flbk_filename = None + if report_xml.module: + # if the default is defined + flbk_filename = pkg_resources.resource_filename( + "odoo.addons.%s" % report_xml.module, + tmpl_name, + ) + elif os.path.isabs(tmpl_name): + # It is an absolute path + flbk_filename = os.path.normcase(os.path.normpath(tmpl_name)) + if flbk_filename and os.path.exists(flbk_filename): + # and it exists on the fileystem + with open(flbk_filename, 'r') as tmpl: + return tmpl.read() + return None + + @api.multi + def _get_template_fallback(self, model_instance): + """ + Return the template referenced in the report definition + :return: + """ + self.ensure_one() + report_xml = self.ir_actions_report_xml_id + return self._get_template_from_path(report_xml.py3o_template_fallback) + + @api.multi + def get_template(self, model_instance): """private helper to fetch the template data either from the database or from the default template file provided by the implementer. @@ -97,7 +131,6 @@ class Py3oReport(models.TransientModel): odoo.exceptions.DeferredException """ self.ensure_one() - tmpl_data = None 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 @@ -105,22 +138,8 @@ class Py3oReport(models.TransientModel): report_xml.py3o_template_id.py3o_template_data ) - elif report_xml.py3o_template_fallback: - tmpl_name = report_xml.py3o_template_fallback - flbk_filename = None - if report_xml.module: - # if the default is defined - flbk_filename = pkg_resources.resource_filename( - "odoo.addons.%s" % report_xml.module, - tmpl_name, - ) - elif os.path.isabs(tmpl_name): - # It is an absolute path - flbk_filename = os.path.normcase(os.path.normpath(tmpl_name)) - if flbk_filename and os.path.exists(flbk_filename): - # and it exists on the fileystem - with open(flbk_filename, 'r') as tmpl: - tmpl_data = tmpl.read() + else: + tmpl_data = self._get_template_fallback(model_instance) if tmpl_data is None: # if for any reason the template is not found @@ -197,7 +216,7 @@ class Py3oReport(models.TransientModel): filetype = report_xml.py3o_filetype result_fd, result_path = tempfile.mkstemp( suffix='.' + filetype, prefix='p3o.report.tmp.') - tmpl_data = self.get_template() + tmpl_data = self.get_template(model_instance) in_stream = StringIO(tmpl_data) with closing(os.fdopen(result_fd, 'w+')) as out_stream: