Browse Source
Merge pull request #2 from acsone/10.0-refactor_py3o-jne
Merge pull request #2 from acsone/10.0-refactor_py3o-jne
10.0 refactor py3o jnepull/78/head
Alexis de Lattre
8 years ago
committed by
GitHub
6 changed files with 367 additions and 269 deletions
-
2.travis.yml
-
1report_py3o/models/__init__.py
-
55report_py3o/models/ir_actions_report_xml.py
-
342report_py3o/models/py3o_report.py
-
220report_py3o/py3o_parser.py
-
16report_py3o/tests/test_report_py3o.py
@ -1,3 +1,4 @@ |
|||
from . import ir_actions_report_xml |
|||
from . import py3o_template |
|||
from . import py3o_server |
|||
from . import py3o_report |
@ -0,0 +1,342 @@ |
|||
# -*- 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 logging |
|||
import os |
|||
from contextlib import closing |
|||
|
|||
import pkg_resources |
|||
import requests |
|||
import sys |
|||
import tempfile |
|||
from zipfile import ZipFile, ZIP_DEFLATED |
|||
|
|||
from odoo.exceptions import AccessError |
|||
from odoo.exceptions import UserError |
|||
from odoo.report.report_sxw import rml_parse |
|||
from odoo 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: |
|||
from py3o.formats import Formats |
|||
except ImportError: |
|||
logger.debug('Cannot import py3o.formats') |
|||
|
|||
|
|||
_extender_functions = {} |
|||
|
|||
|
|||
class TemplateNotFound(Exception): |
|||
pass |
|||
|
|||
|
|||
def py3o_report_extender(report_xml_id=None): |
|||
""" |
|||
A decorator to define function to extend the context sent to a template. |
|||
This will be called at the creation of the report. |
|||
The following arguments will be passed to it: |
|||
- ir_report: report instance |
|||
- localcontext: The context that will be passed to the report engine |
|||
If no report_xml_id is given the extender is registered for all py3o |
|||
reports |
|||
Idea copied from CampToCamp report_webkit module. |
|||
|
|||
:param report_xml_id: xml id of the report |
|||
:return: a decorated class |
|||
""" |
|||
global _extender_functions |
|||
|
|||
def fct1(fct): |
|||
_extender_functions.setdefault(report_xml_id, []).append(fct) |
|||
return fct |
|||
return fct1 |
|||
|
|||
|
|||
@py3o_report_extender() |
|||
def defautl_extend(report_xml, localcontext): |
|||
# add the base64decode function to be able do decode binary fields into |
|||
# the template |
|||
localcontext['b64decode'] = b64decode |
|||
localcontext['report_xml'] = report_xml |
|||
|
|||
|
|||
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 |
|||
) |
|||
|
|||
@api.multi |
|||
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. |
|||
|
|||
ATM this method takes a report definition recordset |
|||
to try and fetch the report template from database. If not found it |
|||
will fallback to the template file referenced in the report definition. |
|||
|
|||
@returns: string or buffer containing the template data |
|||
|
|||
@raises: TemplateNotFound which is a subclass of |
|||
odoo.exceptions.DeferredException |
|||
""" |
|||
self.ensure_one() |
|||
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_xml.py3o_template_id.py3o_template_data |
|||
) |
|||
|
|||
else: |
|||
tmpl_data = self._get_template_fallback(model_instance) |
|||
|
|||
if tmpl_data is None: |
|||
# if for any reason the template is not found |
|||
raise TemplateNotFound( |
|||
_(u'No template found. Aborting.'), |
|||
sys.exc_info(), |
|||
) |
|||
|
|||
return tmpl_data |
|||
|
|||
@api.multi |
|||
def _extend_parser_context(self, context_instance, report_xml): |
|||
# add default extenders |
|||
for fct in _extender_functions.get(None, []): |
|||
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, 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.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): |
|||
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): |
|||
""" This function to generate our py3o report |
|||
""" |
|||
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(model_instance) |
|||
|
|||
in_stream = StringIO(tmpl_data) |
|||
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, |
|||
} |
|||
fields = { |
|||
"targetformat": filetype, |
|||
"datadict": json.dumps(datadict), |
|||
"image_mapping": "{}", |
|||
} |
|||
if report_xml.py3o_is_local_fusion: |
|||
fields['skipfusion'] = '1' |
|||
r = requests.post( |
|||
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 UserError( |
|||
_('Fusion server error %s') % r.text, |
|||
) |
|||
|
|||
chunk_size = 1024 |
|||
with open(result_path, 'w+') as fd: |
|||
for chunk in r.iter_content(chunk_size): |
|||
fd.write(chunk) |
|||
self._postprocess_report( |
|||
result_path, model_instance.id, save_in_attachment) |
|||
return result_path |
|||
|
|||
@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, reports_path): |
|||
self.ensure_one() |
|||
zfname_prefix = self.ir_actions_report_xml_id.name |
|||
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) |
|||
|
|||
cpt += 1 |
|||
return result_path |
|||
|
|||
@api.multi |
|||
def _merge_results(self, reports_path): |
|||
self.ensure_one() |
|||
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_pdf(reports_path), formats.FORMAT_PDF |
|||
else: |
|||
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): |
|||
""" Override this function to handle our py3o report |
|||
""" |
|||
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 {} |
|||
reports_path = [] |
|||
for model_instance in model_instances: |
|||
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 |
@ -1,220 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright 2013 XCG Consulting (http://odoo.consulting) |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) |
|||
from cStringIO import StringIO |
|||
import json |
|||
import pkg_resources |
|||
import os |
|||
import sys |
|||
from base64 import b64decode |
|||
import requests |
|||
from tempfile import NamedTemporaryFile |
|||
from odoo import api, _ |
|||
from odoo import exceptions |
|||
from odoo.report.report_sxw import report_sxw |
|||
import logging |
|||
|
|||
logger = logging.getLogger(__name__) |
|||
|
|||
try: |
|||
from py3o.template.helpers import Py3oConvertor |
|||
from py3o.template import Template |
|||
except ImportError: |
|||
logger.debug('Cannot import py3o.template') |
|||
try: |
|||
from py3o.formats import Formats |
|||
except ImportError: |
|||
logger.debug('Cannot import py3o.formats') |
|||
|
|||
|
|||
_extender_functions = {} |
|||
|
|||
|
|||
class TemplateNotFound(Exception): |
|||
pass |
|||
|
|||
|
|||
def py3o_report_extender(report_xml_id=None): |
|||
""" |
|||
A decorator to define function to extend the context sent to a template. |
|||
This will be called at the creation of the report. |
|||
The following arguments will be passed to it: |
|||
- ir_report: report instance |
|||
- localcontext: The context that will be passed to the report engine |
|||
If no report_xml_id is given the extender is registered for all py3o |
|||
reports |
|||
Idea copied from CampToCamp report_webkit module. |
|||
|
|||
:param report_xml_id: xml id of the report |
|||
:return: a decorated class |
|||
""" |
|||
global _extender_functions |
|||
|
|||
def fct1(fct): |
|||
_extender_functions.setdefault(report_xml_id, []).append(fct) |
|||
return fct |
|||
return fct1 |
|||
|
|||
|
|||
@py3o_report_extender() |
|||
def defautl_extend(report_xml, localcontext): |
|||
# add the base64decode function to be able do decode binary fields into |
|||
# the template |
|||
localcontext['b64decode'] = b64decode |
|||
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.""" |
|||
|
|||
def get_template(self, report_obj): |
|||
"""private helper to fetch the template data either from the database |
|||
or from the default template file provided by the implementer. |
|||
|
|||
ATM this method takes a report definition recordset |
|||
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 |
|||
""" |
|||
|
|||
tmpl_data = None |
|||
|
|||
if report_obj.py3o_template_id and report_obj.py3o_template_id.id: |
|||
# if a user gave a report template |
|||
tmpl_data = b64decode( |
|||
report_obj.py3o_template_id.py3o_template_data |
|||
) |
|||
|
|||
elif report_obj.py3o_template_fallback: |
|||
tmpl_name = report_obj.py3o_template_fallback |
|||
flbk_filename = None |
|||
if report_obj.module: |
|||
# if the default is defined |
|||
flbk_filename = pkg_resources.resource_filename( |
|||
"odoo.addons.%s" % report_obj.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() |
|||
|
|||
if tmpl_data is None: |
|||
# if for any reason the template is not found |
|||
raise TemplateNotFound( |
|||
_(u'No template found. Aborting.'), |
|||
sys.exc_info(), |
|||
) |
|||
|
|||
return tmpl_data |
|||
|
|||
def _extend_parser_context(self, parser_instance, report_xml): |
|||
# add default extenders |
|||
for fct in _extender_functions.get(None, []): |
|||
fct(report_xml, parser_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) |
|||
|
|||
def create_single_pdf(self, cr, uid, ids, data, report_xml, context=None): |
|||
""" Overide 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) |
|||
|
|||
tmpl_data = self.get_template(report_xml) |
|||
|
|||
in_stream = StringIO(tmpl_data) |
|||
out_stream = StringIO() |
|||
template = Template(in_stream, out_stream) |
|||
localcontext = parser_instance.localcontext |
|||
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) |
|||
files = { |
|||
'tmpl_file': in_stream, |
|||
} |
|||
fields = { |
|||
"targetformat": filetype, |
|||
"datadict": json.dumps(datadict), |
|||
"image_mapping": "{}", |
|||
} |
|||
if report_xml.py3o_is_local_fusion: |
|||
fields['skipfusion'] = '1' |
|||
r = requests.post( |
|||
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( |
|||
_('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: |
|||
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() |
|||
|
|||
return res, filetype |
|||
|
|||
def create(self, cr, uid, ids, data, context=None): |
|||
""" 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 |
Write
Preview
Loading…
Cancel
Save
Reference in new issue