Browse Source

[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
pull/347/head
Laurent Mignon 8 years ago
committed by Laurent Mignon (ACSONE)
parent
commit
990e66123a
  1. 1
      report_py3o/models/__init__.py
  2. 53
      report_py3o/models/ir_actions_report_xml.py
  3. 189
      report_py3o/models/py3o_report.py
  4. 9
      report_py3o/tests/test_report_py3o.py

1
report_py3o/models/__init__.py

@ -1,3 +1,4 @@
from . import ir_actions_report_xml from . import ir_actions_report_xml
from . import py3o_template from . import py3o_template
from . import py3o_server from . import py3o_server
from . import py3o_report

53
report_py3o/models/ir_actions_report_xml.py

@ -1,13 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2013 XCG Consulting (http://odoo.consulting) # Copyright 2013 XCG Consulting (http://odoo.consulting)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import os
import logging import logging
from odoo import api, fields, models, _ from odoo import api, fields, models, _
from odoo.report.interface import report_int from odoo.report.interface import report_int
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
from odoo import addons from odoo import addons
from ..py3o_parser import Py3oParser
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -85,43 +83,14 @@ class IrActionsReportXml(models.Model):
)) ))
report_type = fields.Selection(selection_add=[('py3o', "Py3o")]) 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)

189
report_py3o/py3o_parser.py → report_py3o/models/py3o_report.py

@ -1,24 +1,29 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2013 XCG Consulting (http://odoo.consulting) # Copyright 2013 XCG Consulting (http://odoo.consulting)
# Copyright 2016 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
import base64
from base64 import b64decode
from cStringIO import StringIO from cStringIO import StringIO
import json import json
import pkg_resources
import logging
import os import os
import sys
from base64 import b64decode
import pkg_resources
import requests import requests
import sys
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from odoo import api, _
from odoo import exceptions from odoo import exceptions
from odoo.report.report_sxw import report_sxw from odoo.report.report_sxw import report_sxw
import logging import logging
from zipfile import ZipFile, ZIP_DEFLATED
from openerp import api, fields, models, _
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
try: try:
from py3o.template.helpers import Py3oConvertor from py3o.template.helpers import Py3oConvertor
from py3o.template import Template from py3o.template import Template
from py3o import formats
except ImportError: except ImportError:
logger.debug('Cannot import py3o.template') logger.debug('Cannot import py3o.template')
try: try:
@ -64,11 +69,18 @@ def defautl_extend(report_xml, localcontext):
localcontext['report_xml'] = report_xml 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 """private helper to fetch the template data either from the database
or from the default template file provided by the implementer. 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 to try and fetch the report template from database. If not found it
will fallback to the template file referenced in the report definition. 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 @returns: string or buffer containing the template data
@raises: TemplateNotFound which is a subclass of @raises: TemplateNotFound which is a subclass of
odoo.exceptions.DeferredException odoo.exceptions.DeferredException
""" """
self.ensure_one()
tmpl_data = None 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 # if a user gave a report template
tmpl_data = b64decode( 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 flbk_filename = None
if report_obj.module:
if report_xml.module:
# if the default is defined # if the default is defined
flbk_filename = pkg_resources.resource_filename( flbk_filename = pkg_resources.resource_filename(
"odoo.addons.%s" % report_obj.module,
"odoo.addons.%s" % report_xml.module,
tmpl_name, tmpl_name,
) )
elif os.path.isabs(tmpl_name): elif os.path.isabs(tmpl_name):
@ -119,37 +128,54 @@ class Py3oParser(report_sxw):
return tmpl_data 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 # add default extenders
for fct in _extender_functions.get(None, []): 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 # add extenders for registered on the template
xml_id = report_xml.get_external_id().get(report_xml.id) xml_id = report_xml.get_external_id().get(report_xml.id)
if xml_id in _extender_functions: if xml_id in _extender_functions:
for fct in _extender_functions[xml_id]: 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) in_stream = StringIO(tmpl_data)
out_stream = StringIO() out_stream = StringIO()
template = Template(in_stream, out_stream, escape_false=True) 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: if report_xml.py3o_is_local_fusion:
template.render(localcontext) template.render(localcontext)
in_stream = out_stream in_stream = out_stream
@ -181,7 +207,7 @@ class Py3oParser(report_sxw):
report_xml.py3o_server_id.url, data=fields, files=files) report_xml.py3o_server_id.url, data=fields, files=files)
if r.status_code != 200: if r.status_code != 200:
# server says we have an issue... let's tell that to enduser # server says we have an issue... let's tell that to enduser
raise exceptions.Warning(
raise UserError(
_('Fusion server error %s') % r.text, _('Fusion server error %s') % r.text,
) )
@ -189,32 +215,79 @@ class Py3oParser(report_sxw):
# we do nice chunked reading from the network... # we do nice chunked reading from the network...
chunk_size = 1024 chunk_size = 1024
with NamedTemporaryFile( with NamedTemporaryFile(
suffix=filetype,
prefix='py3o-template-'
suffix=filetype,
prefix='py3o-template-'
) as fd: ) as fd:
for chunk in r.iter_content(chunk_size): for chunk in r.iter_content(chunk_size):
fd.write(chunk) fd.write(chunk)
fd.seek(0) fd.seek(0)
# ... but odoo wants the whole data in memory anyways :) # ... but odoo wants the whole data in memory anyways :)
res = fd.read() 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 """ 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)

9
report_py3o/tests/test_report_py3o.py

@ -11,7 +11,7 @@ from py3o.formats import Formats
from odoo.tests.common import TransactionCase from odoo.tests.common import TransactionCase
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
from ..py3o_parser import TemplateNotFound
from ..models.py3o_report import TemplateNotFound
from base64 import b64encode from base64 import b64encode
@ -56,9 +56,10 @@ class TestReportPy3o(TransactionCase):
"Field 'Output Format' is required for Py3O report") "Field 'Output Format' is required for Py3O report")
def test_reports(self): def test_reports(self):
py3o_report = self.env['py3o.report']
report = self.env.ref("report_py3o.res_users_report_py3o") 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 # test the call the the create method inside our custom parser
report.render_report(self.env.user.ids, report.render_report(self.env.user.ids,
report.report_name, report.report_name,
@ -98,7 +99,7 @@ class TestReportPy3o(TransactionCase):
report.render_report( report.render_report(
self.env.user.ids, report.report_name, {}) 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 report.py3o_template_fallback = flbk_filename
res = report.render_report( res = report.render_report(
self.env.user.ids, report.report_name, {}) self.env.user.ids, report.report_name, {})

Loading…
Cancel
Save