Skip to content

Commit 506bcab

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 506bcab

4 files changed

Lines changed: 152 additions & 118 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: 133 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,29 @@
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+
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, _
1720

1821
logger = logging.getLogger(__name__)
1922

2023
try:
2124
from py3o.template.helpers import Py3oConvertor
2225
from py3o.template import Template
26+
from py3o import formats
2327
except ImportError:
2428
logger.debug('Cannot import py3o.template')
2529
try:
@@ -64,42 +68,46 @@ def defautl_extend(report_xml, localcontext):
6468
localcontext['b64decode'] = b64decode
6569

6670

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

71-
def get_template(self, report_obj):
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):
7283
"""private helper to fetch the template data either from the database
7384
or from the default template file provided by the implementer.
7485
7586
ATM this method takes a report definition recordset
7687
to try and fetch the report template from database. If not found it
7788
will fallback to the template file referenced in the report definition.
7889
79-
@param report_obj: a recordset representing the report defintion
80-
@type report_obj: openerp.model.recordset instance
81-
8290
@returns: string or buffer containing the template data
8391
8492
@raises: TemplateNotFound which is a subclass of
8593
openerp.exceptions.DeferredException
8694
"""
87-
95+
self.ensure_one()
8896
tmpl_data = None
89-
90-
if report_obj.py3o_template_id and report_obj.py3o_template_id.id:
97+
report_xml = self.ir_actions_report_xml_id
98+
if report_xml.py3o_template_id and report_xml.py3o_template_id.id:
9199
# if a user gave a report template
92100
tmpl_data = b64decode(
93-
report_obj.py3o_template_id.py3o_template_data
101+
report_xml.py3o_template_id.py3o_template_data
94102
)
95103

96-
elif report_obj.py3o_template_fallback:
97-
tmpl_name = report_obj.py3o_template_fallback
104+
elif report_xml.py3o_template_fallback:
105+
tmpl_name = report_xml.py3o_template_fallback
98106
flbk_filename = None
99-
if report_obj.module:
107+
if report_xml.module:
100108
# if the default is defined
101109
flbk_filename = pkg_resources.resource_filename(
102-
"openerp.addons.%s" % report_obj.module,
110+
"openerp.addons.%s" % report_xml.module,
103111
tmpl_name,
104112
)
105113
elif os.path.isabs(tmpl_name):
@@ -119,37 +127,53 @@ def get_template(self, report_obj):
119127

120128
return tmpl_data
121129

122-
def _extend_parser_context(self, parser_instance, report_xml):
130+
@api.multi
131+
def _extend_parser_context(self, context_instance, report_xml):
123132
# add default extenders
124133
for fct in _extender_functions.get(None, []):
125-
fct(report_xml, parser_instance.localcontext)
134+
fct(report_xml, context_instance.localcontext)
126135
# add extenders for registered on the template
127136
xml_id = report_xml.get_external_id().get(report_xml.id)
128137
if xml_id in _extender_functions:
129138
for fct in _extender_functions[xml_id]:
130-
fct(report_xml, parser_instance.localcontext)
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, content, res_id, save_in_attachment):
154+
if save_in_attachment.get(res_id):
155+
attachment = {
156+
'name': save_in_attachment.get(res_id),
157+
'datas': base64.encodestring(content),
158+
'datas_fname': save_in_attachment.get(res_id),
159+
'res_model': save_in_attachment.get('model'),
160+
'res_id': res_id,
161+
}
162+
return self.env['ir.attachment'].create(attachment)
131163

132-
def create_single_pdf(self, cr, uid, ids, data, report_xml, context=None):
133-
""" Overide this function to generate our py3o report
164+
@api.multi
165+
def _create_single_report(self, model_instance, data, save_in_attachment):
166+
""" This function to generate our py3o report
134167
"""
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-
)
168+
self.ensure_one()
169+
report_xml = self.ir_actions_report_xml_id
139170

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)
146-
147-
tmpl_data = self.get_template(report_xml)
171+
tmpl_data = self.get_template()
148172

149173
in_stream = StringIO(tmpl_data)
150174
out_stream = StringIO()
151175
template = Template(in_stream, out_stream, escape_false=True)
152-
localcontext = parser_instance.localcontext
176+
localcontext = self._get_parser_context(model_instance, data)
153177
if report_xml.py3o_is_local_fusion:
154178
template.render(localcontext)
155179
in_stream = out_stream
@@ -181,46 +205,87 @@ def create_single_pdf(self, cr, uid, ids, data, report_xml, context=None):
181205
report_xml.py3o_server_id.url, data=fields, files=files)
182206
if r.status_code != 200:
183207
# server says we have an issue... let's tell that to enduser
184-
raise exceptions.Warning(
208+
raise UserError(
185209
_('Fusion server error %s') % r.text,
186210
)
187211

188212
# Here is a little joke about Odoo
189213
# we do nice chunked reading from the network...
190214
chunk_size = 1024
191215
with NamedTemporaryFile(
192-
suffix=filetype,
193-
prefix='py3o-template-'
216+
suffix=filetype,
217+
prefix='py3o-template-'
194218
) as fd:
195219
for chunk in r.iter_content(chunk_size):
196220
fd.write(chunk)
197221
fd.seek(0)
198222
# ... but odoo wants the whole data in memory anyways :)
199223
res = fd.read()
224+
self._postprocess_report(
225+
res, model_instance.id, save_in_attachment)
226+
return res, "." + self.ir_actions_report_xml_id.py3o_filetype
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 _zip_results(self, results):
242+
self.ensure_one()
243+
zfname_prefix = self.ir_actions_report_xml_id.name
244+
with NamedTemporaryFile(suffix="zip", prefix='py3o-zip-result') as fd:
245+
with ZipFile(fd, 'w', ZIP_DEFLATED) as zf:
246+
cpt = 0
247+
for r, ext in results:
248+
fname = "%s_%d.%s" % (zfname_prefix, cpt, ext)
249+
zf.writestr(fname, r)
250+
cpt += 1
251+
fd.seek(0)
252+
return fd.read(), 'zip'
253+
254+
@api.multi
255+
def _merge_pdfs(self, results):
256+
from pyPdf import PdfFileWriter, PdfFileReader
257+
output = PdfFileWriter()
258+
for r in results:
259+
reader = PdfFileReader(StringIO(r[0]))
260+
for page in range(reader.getNumPages()):
261+
output.addPage(reader.getPage(page))
262+
s = StringIO()
263+
output.write(s)
264+
return s.getvalue(), formats.FORMAT_PDF
265+
266+
@api.multi
267+
def _merge_results(self, results):
268+
self.ensure_one()
269+
if not results:
270+
return False, False
271+
if len(results) == 1:
272+
return results[0]
273+
filetype = self.ir_actions_report_xml_id.py3o_filetype
274+
if filetype == formats.FORMAT_PDF:
275+
return self._merge_pdfs(results)
276+
else:
277+
return self._zip_results(results)
200278

201-
return res, "." + filetype
202-
203-
def create(self, cr, uid, ids, data, context=None):
279+
@api.multi
280+
def create_report(self, res_ids, data):
204281
""" Override this function to handle our py3o report
205282
"""
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
283+
model_instances = self.env[self.ir_actions_report_xml_id.model].browse(
284+
res_ids)
285+
save_in_attachment = self._check_attachment_use(
286+
model_instances, self.ir_actions_report_xml_id) or {}
287+
results = []
288+
for model_instance in model_instances:
289+
results.append(self._get_or_create_single_report(
290+
model_instance, data, save_in_attachment))
291+
return self._merge_results(results)

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)