Skip to content

Commit 14f1548

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 207bb2e commit 14f1548

4 files changed

Lines changed: 143 additions & 117 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)
Lines changed: 124 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,28 @@
11
# -*- coding: utf-8 -*-
22
# Copyright 2013 XCG Consulting (http://odoo.consulting)
3+
# Copyright 2016 ACSONE SA/NV
34
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
5+
import base64
6+
from base64 import b64decode
47
from cStringIO import StringIO
58
import json
6-
import pkg_resources
9+
import logging
710
import os
8-
import sys
9-
from base64 import b64decode
11+
import pkg_resources
1012
import requests
13+
import sys
1114
from tempfile import NamedTemporaryFile
12-
from openerp import _
13-
from openerp import exceptions
14-
from openerp.report.report_sxw import report_sxw
15-
from openerp import registry
16-
import logging
15+
16+
from openerp.exceptions import UserError
17+
from openerp.report.report_sxw import rml_parse
18+
from openerp import api, fields, models, _
1719

1820
logger = logging.getLogger(__name__)
1921

2022
try:
2123
from py3o.template.helpers import Py3oConvertor
2224
from py3o.template import Template
25+
from py3o import formats
2326
except ImportError:
2427
logger.debug('Cannot import py3o.template')
2528
try:
@@ -64,42 +67,46 @@ def defautl_extend(report_xml, localcontext):
6467
localcontext['b64decode'] = b64decode
6568

6669

67-
class Py3oParser(report_sxw):
68-
"""Custom class that use Py3o to render libroffice reports.
69-
Code partially taken from CampToCamp's webkit_report."""
70+
class Py3oReport(models.TransientModel):
71+
_name = "py3o.report"
72+
_inherit = 'report'
73+
_description = "Report Py30"
7074

71-
def get_template(self, report_obj):
75+
ir_actions_report_xml_id = fields.Many2one(
76+
comodel_name="ir.actions.report.xml",
77+
required=True
78+
)
79+
80+
@api.multi
81+
def get_template(self):
7282
"""private helper to fetch the template data either from the database
7383
or from the default template file provided by the implementer.
7484
7585
ATM this method takes a report definition recordset
7686
to try and fetch the report template from database. If not found it
7787
will fallback to the template file referenced in the report definition.
7888
79-
@param report_obj: a recordset representing the report defintion
80-
@type report_obj: openerp.model.recordset instance
81-
8289
@returns: string or buffer containing the template data
8390
8491
@raises: TemplateNotFound which is a subclass of
8592
openerp.exceptions.DeferredException
8693
"""
87-
94+
self.ensure_one()
8895
tmpl_data = None
89-
90-
if report_obj.py3o_template_id and report_obj.py3o_template_id.id:
96+
report_xml = self.ir_actions_report_xml_id
97+
if report_xml.py3o_template_id and report_xml.py3o_template_id.id:
9198
# if a user gave a report template
9299
tmpl_data = b64decode(
93-
report_obj.py3o_template_id.py3o_template_data
100+
report_xml.py3o_template_id.py3o_template_data
94101
)
95102

96-
elif report_obj.py3o_template_fallback:
97-
tmpl_name = report_obj.py3o_template_fallback
103+
elif report_xml.py3o_template_fallback:
104+
tmpl_name = report_xml.py3o_template_fallback
98105
flbk_filename = None
99-
if report_obj.module:
106+
if report_xml.module:
100107
# if the default is defined
101108
flbk_filename = pkg_resources.resource_filename(
102-
"openerp.addons.%s" % report_obj.module,
109+
"openerp.addons.%s" % report_xml.module,
103110
tmpl_name,
104111
)
105112
elif os.path.isabs(tmpl_name):
@@ -119,37 +126,55 @@ def get_template(self, report_obj):
119126

120127
return tmpl_data
121128

122-
def _extend_parser_context(self, parser_instance, report_xml):
129+
@api.multi
130+
def _extend_parser_context(self, context_instance, report_xml):
123131
# add default extenders
124132
for fct in _extender_functions.get(None, []):
125-
fct(report_xml, parser_instance.localcontext)
133+
fct(report_xml, context_instance.localcontext)
126134
# add extenders for registered on the template
127135
xml_id = report_xml.get_external_id().get(report_xml.id)
128136
if xml_id in _extender_functions:
129137
for fct in _extender_functions[xml_id]:
130-
fct(report_xml, parser_instance.localcontext)
138+
fct(report_xml, context_instance.localcontext)
139+
140+
@api.multi
141+
def _get_parser_context(self, model_instance, data):
142+
report_xml = self.ir_actions_report_xml_id
143+
context_instance = rml_parse(self.env.cr, self.env.uid,
144+
report_xml.name,
145+
context=self.env.context)
146+
context_instance.set_context(model_instance, data, model_instance.ids,
147+
report_xml.report_type)
148+
self._extend_parser_context(context_instance, report_xml)
149+
return context_instance.localcontext
150+
151+
@api.multi
152+
def _postprocess_report(self, res, model_instance, save_in_attachment):
153+
res_id = model_instance.id
154+
if save_in_attachment.get(res_id):
155+
attachment = {
156+
'name': save_in_attachment.get(res_id),
157+
'datas': base64.encodestring(res),
158+
'datas_fname': save_in_attachment.get(res_id),
159+
'res_model': save_in_attachment.get('model'),
160+
'res_id': res_id,
161+
}
162+
self.env['ir.attachment'].create(attachment)
163+
return res, "." + self.ir_actions_report_xml_id.py3o_filetype
131164

132-
def create_single_pdf(self, cr, uid, ids, data, report_xml, context=None):
133-
""" Overide this function to generate our py3o report
165+
@api.multi
166+
def _create_single_report(self, model_instance, data, save_in_attachment):
167+
""" This function to generate our py3o report
134168
"""
135-
if report_xml.report_type != 'py3o':
136-
return super(Py3oParser, self).create_single_pdf(
137-
cr, uid, ids, data, report_xml, context=context
138-
)
139-
140-
parser_instance = self.parser(cr, uid, self.name2, context=context)
141-
parser_instance.set_context(
142-
self.getObjects(cr, uid, ids, context),
143-
data, ids, report_xml.report_type
144-
)
145-
self._extend_parser_context(parser_instance, report_xml)
169+
self.ensure_one()
170+
report_xml = self.ir_actions_report_xml_id
146171

147-
tmpl_data = self.get_template(report_xml)
172+
tmpl_data = self.get_template()
148173

149174
in_stream = StringIO(tmpl_data)
150175
out_stream = StringIO()
151176
template = Template(in_stream, out_stream, escape_false=True)
152-
localcontext = parser_instance.localcontext
177+
localcontext = self._get_parser_context(model_instance, data)
153178
if report_xml.py3o_is_local_fusion:
154179
template.render(localcontext)
155180
in_stream = out_stream
@@ -181,7 +206,7 @@ def create_single_pdf(self, cr, uid, ids, data, report_xml, context=None):
181206
report_xml.py3o_server_id.url, data=fields, files=files)
182207
if r.status_code != 200:
183208
# server says we have an issue... let's tell that to enduser
184-
raise exceptions.Warning(
209+
raise UserError(
185210
_('Fusion server error %s') % r.text,
186211
)
187212

@@ -197,30 +222,62 @@ def create_single_pdf(self, cr, uid, ids, data, report_xml, context=None):
197222
fd.seek(0)
198223
# ... but odoo wants the whole data in memory anyways :)
199224
res = fd.read()
200-
201-
return res, "." + filetype
202-
203-
def create(self, cr, uid, ids, data, context=None):
225+
return self._postprocess_report(
226+
res, model_instance, save_in_attachment)
227+
228+
@api.multi
229+
def _get_or_create_single_report(self, model_instance, data,
230+
save_in_attachment):
231+
self.ensure_one()
232+
if save_in_attachment and save_in_attachment[
233+
'loaded_documents'].get(model_instance.id):
234+
d = save_in_attachment[
235+
'loaded_documents'].get(model_instance.id)
236+
return d, self.ir_actions_report_xml_id.py3o_filetype
237+
return self._create_single_report(
238+
model_instance, data, save_in_attachment)
239+
240+
@api.multi
241+
def create_report(self, res_ids, data):
204242
""" Override this function to handle our py3o report
205243
"""
206-
pool = registry(cr.dbname)
207-
ir_action_report_obj = pool['ir.actions.report.xml']
208-
report_xml_ids = ir_action_report_obj.search(
209-
cr, uid, [('report_name', '=', self.name[7:])], context=context
210-
)
211-
if not report_xml_ids:
212-
return super(Py3oParser, self).create(
213-
cr, uid, ids, data, context=context
214-
)
215-
216-
report_xml = ir_action_report_obj.browse(
217-
cr, uid, report_xml_ids[0], context=context
218-
)
219-
220-
result = self.create_source_pdf(
221-
cr, uid, ids, data, report_xml, context
222-
)
223-
224-
if not result:
225-
return False, False
226-
return result
244+
if len(res_ids) > 1 and self.ir_actions_report_xml_id.py3o_filetype \
245+
!= formats.FORMAT_PDF:
246+
raise UserError(
247+
_('Generating multiple reports at once is only supported '
248+
'with output format %s') % formats.FORMAT_PDF)
249+
model_instances = self.env[self.ir_actions_report_xml_id.model].browse(
250+
res_ids)
251+
save_in_attachment = self._check_attachment_use(
252+
model_instances, self.ir_actions_report_xml_id) or {}
253+
if len(model_instances) == 1:
254+
return self._get_or_create_single_report(
255+
model_instances[0], data, save_in_attachment)
256+
results = []
257+
for model_instance in model_instances:
258+
results.add(self._get_or_create_single_report(
259+
model_instance, data, save_in_attachment))
260+
if results:
261+
from pyPdf import PdfFileWriter, PdfFileReader
262+
output = PdfFileWriter()
263+
for r in results:
264+
reader = PdfFileReader(StringIO(r[0]))
265+
for page in range(reader.getNumPages()):
266+
output.addPage(reader.getPage(page))
267+
s = StringIO()
268+
output.write(s)
269+
return s.getvalue(), results[0][1]
270+
return False, False
271+
272+
# the 2 overrides below are required since in the base class the v8 method
273+
# calls the v7 one with the context as parameter but the v7 one has not
274+
# the context declared in the these paramaters
275+
@api.v7
276+
def _check_attachment_use(self, cr, uid, ids, report):
277+
return super(Py3oReport, self)._check_attachment_use(
278+
cr, uid, ids, report)
279+
280+
@api.v8
281+
def _check_attachment_use(self, records, report):
282+
return Py3oReport._check_attachment_use(
283+
self._model, self._cr, self._uid, records.ids, report)

report_py3o/tests/test_report_py3o.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from openerp.tests.common import TransactionCase
1212
from openerp.exceptions import ValidationError
1313

14-
from ..py3o_parser import TemplateNotFound
14+
from ..models.py3o_report import TemplateNotFound
1515
from base64 import b64encode
1616

1717

@@ -56,9 +56,10 @@ def test_required_py3_filetype(self):
5656
"Field 'Output Format' is required for Py3O report")
5757

5858
def test_reports(self):
59+
py3o_report = self.env['py3o.report']
5960
report = self.env.ref("report_py3o.res_users_report_py3o")
60-
with mock.patch('openerp.addons.report_py3o.py3o_parser.'
61-
'Py3oParser.create_single_pdf') as patched_pdf:
61+
with mock.patch.object(
62+
py3o_report.__class__, '_create_single_report') as patched_pdf:
6263
# test the call the the create method inside our custom parser
6364
report.render_report(self.env.user.ids,
6465
report.report_name,
@@ -98,7 +99,7 @@ def test_report_template_configs(self):
9899
report.render_report(
99100
self.env.user.ids, report.report_name, {})
100101

101-
# the template can also be provivided as an abspaath
102+
# the template can also be provided as an abspaath
102103
report.py3o_template_fallback = flbk_filename
103104
res = report.render_report(
104105
self.env.user.ids, report.report_name, {})

0 commit comments

Comments
 (0)