Skip to content

Commit 51c20af

Browse files
committed
[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
1 parent b740945 commit 51c20af

5 files changed

Lines changed: 324 additions & 276 deletions

File tree

report_py3o/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from . import ir_actions_report_xml
22
from . import py3o_template
33
from . import py3o_server
4+
from . import py3o_report

report_py3o/models/ir_actions_report_xml.py

Lines changed: 13 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
# -*- coding: utf-8 -*-
22
# Copyright 2013 XCG Consulting (http://odoo.consulting)
33
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4-
import os
54
import logging
6-
from openerp import api, fields, models, SUPERUSER_ID, _
7-
from openerp.report.interface import report_int
5+
from openerp import api, fields, models, _
86
from openerp.exceptions import ValidationError
9-
from openerp import addons
10-
from ..py3o_parser import Py3oParser
7+
118

129
logger = logging.getLogger(__name__)
1310

@@ -85,44 +82,14 @@ def _get_py3o_filetypes(self):
8582
))
8683
report_type = fields.Selection(selection_add=[('py3o', "Py3o")])
8784

88-
@api.cr
89-
def _lookup_report(self, cr, name):
90-
"""Look up a report definition.
91-
"""
92-
93-
# First lookup in the deprecated place, because if the report
94-
# definition has not been updated, it is more likely the correct
95-
# definition is there. Only reports with custom parser
96-
# specified in Python are still there.
97-
if 'report.' + name in report_int._reports:
98-
new_report = report_int._reports['report.' + name]
99-
if not isinstance(new_report, Py3oParser):
100-
new_report = None
101-
else:
102-
report_data = self.search_read(
103-
cr, SUPERUSER_ID,
104-
[("report_name", "=", name),
105-
("report_type", "=", "py3o")],
106-
['parser', 'model', 'report_name', 'report_rml', 'header'],
107-
limit=1)
108-
if report_data:
109-
report_data = report_data[0]
110-
kwargs = {}
111-
if report_data['parser']:
112-
kwargs['parser'] = getattr(addons, report_data['parser'])
113-
114-
new_report = Py3oParser(
115-
'report.' + report_data['report_name'],
116-
report_data['model'],
117-
os.path.join('addons', report_data['report_rml'] or '/'),
118-
header=report_data['header'],
119-
register=False,
120-
**kwargs
121-
)
122-
else:
123-
new_report = None
124-
125-
if new_report:
126-
return new_report
127-
else:
128-
return super(IrActionsReportXml, self)._lookup_report(cr, name)
85+
@api.model
86+
def render_report(self, res_ids, name, data):
87+
action_py3o_report = self.search(
88+
[("report_name", "=", name),
89+
("report_type", "=", "py3o")])
90+
if action_py3o_report:
91+
return self.env['py3o.report'].create({
92+
'ir_actions_report_xml_id': action_py3o_report.id
93+
}).create_report(res_ids, data)
94+
return super(IrActionsReportXml, self).render_report(
95+
res_ids, name, data)

report_py3o/models/py3o_report.py

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2013 XCG Consulting (http://odoo.consulting)
3+
# Copyright 2016 ACSONE SA/NV
4+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
5+
import base64
6+
from base64 import b64decode
7+
from cStringIO import StringIO
8+
import json
9+
import logging
10+
import os
11+
import pkg_resources
12+
import requests
13+
import sys
14+
from tempfile import NamedTemporaryFile
15+
from zipfile import ZipFile, ZIP_DEFLATED
16+
17+
from openerp.exceptions import UserError
18+
from openerp.report.report_sxw import rml_parse
19+
from openerp import api, fields, models, _
20+
21+
logger = logging.getLogger(__name__)
22+
23+
try:
24+
from py3o.template.helpers import Py3oConvertor
25+
from py3o.template import Template
26+
from py3o import formats
27+
except ImportError:
28+
logger.debug('Cannot import py3o.template')
29+
try:
30+
from py3o.formats import Formats
31+
except ImportError:
32+
logger.debug('Cannot import py3o.formats')
33+
34+
35+
_extender_functions = {}
36+
37+
38+
class TemplateNotFound(Exception):
39+
pass
40+
41+
42+
def py3o_report_extender(report_xml_id=None):
43+
"""
44+
A decorator to define function to extend the context sent to a template.
45+
This will be called at the creation of the report.
46+
The following arguments will be passed to it:
47+
- ir_report: report instance
48+
- localcontext: The context that will be passed to the report engine
49+
If no report_xml_id is given the extender is registered for all py3o
50+
reports
51+
Idea copied from CampToCamp report_webkit module.
52+
53+
:param report_xml_id: xml id of the report
54+
:return: a decorated class
55+
"""
56+
global _extender_functions
57+
58+
def fct1(fct):
59+
_extender_functions.setdefault(report_xml_id, []).append(fct)
60+
return fct
61+
return fct1
62+
63+
64+
@py3o_report_extender()
65+
def defautl_extend(report_xml, localcontext):
66+
# add the base64decode function to be able do decode binary fields into
67+
# the template
68+
localcontext['b64decode'] = b64decode
69+
70+
71+
class Py3oReport(models.TransientModel):
72+
_name = "py3o.report"
73+
_inherit = 'report'
74+
_description = "Report Py30"
75+
76+
ir_actions_report_xml_id = fields.Many2one(
77+
comodel_name="ir.actions.report.xml",
78+
required=True
79+
)
80+
81+
@api.multi
82+
def get_template(self):
83+
"""private helper to fetch the template data either from the database
84+
or from the default template file provided by the implementer.
85+
86+
ATM this method takes a report definition recordset
87+
to try and fetch the report template from database. If not found it
88+
will fallback to the template file referenced in the report definition.
89+
90+
@returns: string or buffer containing the template data
91+
92+
@raises: TemplateNotFound which is a subclass of
93+
openerp.exceptions.DeferredException
94+
"""
95+
self.ensure_one()
96+
tmpl_data = None
97+
report_xml = self.ir_actions_report_xml_id
98+
if report_xml.py3o_template_id and report_xml.py3o_template_id.id:
99+
# if a user gave a report template
100+
tmpl_data = b64decode(
101+
report_xml.py3o_template_id.py3o_template_data
102+
)
103+
104+
elif report_xml.py3o_template_fallback:
105+
tmpl_name = report_xml.py3o_template_fallback
106+
flbk_filename = None
107+
if report_xml.module:
108+
# if the default is defined
109+
flbk_filename = pkg_resources.resource_filename(
110+
"openerp.addons.%s" % report_xml.module,
111+
tmpl_name,
112+
)
113+
elif os.path.isabs(tmpl_name):
114+
# It is an absolute path
115+
flbk_filename = os.path.normcase(os.path.normpath(tmpl_name))
116+
if flbk_filename and os.path.exists(flbk_filename):
117+
# and it exists on the fileystem
118+
with open(flbk_filename, 'r') as tmpl:
119+
tmpl_data = tmpl.read()
120+
121+
if tmpl_data is None:
122+
# if for any reason the template is not found
123+
raise TemplateNotFound(
124+
_(u'No template found. Aborting.'),
125+
sys.exc_info(),
126+
)
127+
128+
return tmpl_data
129+
130+
@api.multi
131+
def _extend_parser_context(self, context_instance, report_xml):
132+
# add default extenders
133+
for fct in _extender_functions.get(None, []):
134+
fct(report_xml, context_instance.localcontext)
135+
# add extenders for registered on the template
136+
xml_id = report_xml.get_external_id().get(report_xml.id)
137+
if xml_id in _extender_functions:
138+
for fct in _extender_functions[xml_id]:
139+
fct(report_xml, context_instance.localcontext)
140+
141+
@api.multi
142+
def _get_parser_context(self, model_instance, data):
143+
report_xml = self.ir_actions_report_xml_id
144+
context_instance = rml_parse(self.env.cr, self.env.uid,
145+
report_xml.name,
146+
context=self.env.context)
147+
context_instance.set_context(model_instance, data, model_instance.ids,
148+
report_xml.report_type)
149+
self._extend_parser_context(context_instance, report_xml)
150+
return context_instance.localcontext
151+
152+
@api.multi
153+
def _postprocess_report(self, res, model_instance, save_in_attachment):
154+
res_id = model_instance.id
155+
if save_in_attachment.get(res_id):
156+
attachment = {
157+
'name': save_in_attachment.get(res_id),
158+
'datas': base64.encodestring(res),
159+
'datas_fname': save_in_attachment.get(res_id),
160+
'res_model': save_in_attachment.get('model'),
161+
'res_id': res_id,
162+
}
163+
self.env['ir.attachment'].create(attachment)
164+
return res, "." + self.ir_actions_report_xml_id.py3o_filetype
165+
166+
@api.multi
167+
def _create_single_report(self, model_instance, data, save_in_attachment):
168+
""" This function to generate our py3o report
169+
"""
170+
self.ensure_one()
171+
report_xml = self.ir_actions_report_xml_id
172+
173+
tmpl_data = self.get_template()
174+
175+
in_stream = StringIO(tmpl_data)
176+
out_stream = StringIO()
177+
template = Template(in_stream, out_stream, escape_false=True)
178+
localcontext = self._get_parser_context(model_instance, data)
179+
if report_xml.py3o_is_local_fusion:
180+
template.render(localcontext)
181+
in_stream = out_stream
182+
datadict = {}
183+
else:
184+
expressions = template.get_all_user_python_expression()
185+
py_expression = template.convert_py3o_to_python_ast(expressions)
186+
convertor = Py3oConvertor()
187+
data_struct = convertor(py_expression)
188+
datadict = data_struct.render(localcontext)
189+
190+
filetype = report_xml.py3o_filetype
191+
is_native = Formats().get_format(filetype).native
192+
if is_native:
193+
res = out_stream.getvalue()
194+
else: # Call py3o.server to render the template in the desired format
195+
in_stream.seek(0)
196+
files = {
197+
'tmpl_file': in_stream,
198+
}
199+
fields = {
200+
"targetformat": filetype,
201+
"datadict": json.dumps(datadict),
202+
"image_mapping": "{}",
203+
}
204+
if report_xml.py3o_is_local_fusion:
205+
fields['skipfusion'] = '1'
206+
r = requests.post(
207+
report_xml.py3o_server_id.url, data=fields, files=files)
208+
if r.status_code != 200:
209+
# server says we have an issue... let's tell that to enduser
210+
raise UserError(
211+
_('Fusion server error %s') % r.text,
212+
)
213+
214+
# Here is a little joke about Odoo
215+
# we do nice chunked reading from the network...
216+
chunk_size = 1024
217+
with NamedTemporaryFile(
218+
suffix=filetype,
219+
prefix='py3o-template-'
220+
) as fd:
221+
for chunk in r.iter_content(chunk_size):
222+
fd.write(chunk)
223+
fd.seek(0)
224+
# ... but odoo wants the whole data in memory anyways :)
225+
res = fd.read()
226+
return self._postprocess_report(
227+
res, model_instance, save_in_attachment)
228+
229+
@api.multi
230+
def _get_or_create_single_report(self, model_instance, data,
231+
save_in_attachment):
232+
self.ensure_one()
233+
if save_in_attachment and save_in_attachment[
234+
'loaded_documents'].get(model_instance.id):
235+
d = save_in_attachment[
236+
'loaded_documents'].get(model_instance.id)
237+
return d, self.ir_actions_report_xml_id.py3o_filetype
238+
return self._create_single_report(
239+
model_instance, data, save_in_attachment)
240+
241+
@api.multi
242+
def _zip_results(self, results):
243+
self.ensure_one()
244+
zfname_prefix = self.ir_actions_report_xml_id.name
245+
with NamedTemporaryFile(suffix="zip", prefix='py3o-zip-result') as fd:
246+
with ZipFile(fd, 'w', ZIP_DEFLATED) as zf:
247+
cpt = 0
248+
for r, ext in results:
249+
fname = "%s_%d.%s" % (zfname_prefix, cpt, ext)
250+
zf.writestr(fname, r)
251+
cpt += 1
252+
fd.seek(0)
253+
return fd.read(), 'zip'
254+
255+
@api.multi
256+
def _merge_pdfs(self, results):
257+
from pyPdf import PdfFileWriter, PdfFileReader
258+
output = PdfFileWriter()
259+
for r in results:
260+
reader = PdfFileReader(StringIO(r[0]))
261+
for page in range(reader.getNumPages()):
262+
output.addPage(reader.getPage(page))
263+
s = StringIO()
264+
output.write(s)
265+
return s.getvalue(), formats.FORMAT_PDF
266+
267+
@api.multi
268+
def _merge_results(self, results):
269+
self.ensure_one()
270+
if not results:
271+
return False, False
272+
if len(results) == 1:
273+
return results[0]
274+
filetype = self.ir_actions_report_xml_id.py3o_filetype
275+
if filetype == formats.FORMAT_PDF:
276+
return self._merge_pdfs(results)
277+
else:
278+
return self._zip_results(results)
279+
280+
@api.multi
281+
def create_report(self, res_ids, data):
282+
""" Override this function to handle our py3o report
283+
"""
284+
model_instances = self.env[self.ir_actions_report_xml_id.model].browse(
285+
res_ids)
286+
save_in_attachment = self._check_attachment_use(
287+
model_instances, self.ir_actions_report_xml_id) or {}
288+
results = []
289+
for model_instance in model_instances:
290+
results.append(self._get_or_create_single_report(
291+
model_instance, data, save_in_attachment))
292+
return self._merge_results(results)
293+
294+
# the 2 overrides below are required since in the base class the v8 method
295+
# calls the v7 one with the context as parameter but the v7 one has not
296+
# the context declared in the these paramaters
297+
@api.v7
298+
def _check_attachment_use(self, cr, uid, ids, report):
299+
return super(Py3oReport, self)._check_attachment_use(
300+
cr, uid, ids, report)
301+
302+
@api.v8
303+
def _check_attachment_use(self, records, report):
304+
return Py3oReport._check_attachment_use(
305+
self._model, self._cr, self._uid, records.ids, report)

0 commit comments

Comments
 (0)