Skip to content

Commit 441483d

Browse files
lmignonJonathan Nemry (ACSONE)
authored andcommitted
[IMP] Minimizes memory consumption
Conflicts: report_py3o/models/py3o_report.py
1 parent 36c9aa8 commit 441483d

File tree

2 files changed

+113
-77
lines changed

2 files changed

+113
-77
lines changed

report_py3o/models/py3o_report.py

Lines changed: 106 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
import json
99
import logging
1010
import os
11+
from contextlib import closing
12+
1113
import pkg_resources
1214
import requests
1315
import sys
14-
from tempfile import NamedTemporaryFile
15-
import logging
16+
import tempfile
1617
from zipfile import ZipFile, ZIP_DEFLATED
1718

19+
from odoo.exceptions import AccessError
1820
from odoo.exceptions import UserError
1921
from openerp import api, fields, models, _
2022
from odoo.report.report_sxw import rml_parse
@@ -151,49 +153,72 @@ def _get_parser_context(self, model_instance, data):
151153
self._extend_parser_context(context_instance, report_xml)
152154
return context_instance.localcontext
153155

154-
@api.multi
155-
def _postprocess_report(self, content, res_id, save_in_attachment):
156+
@api.model
157+
def _get_report_from_name(self, report_name):
158+
"""Get the first record of ir.actions.report.xml having the
159+
``report_name`` as value for the field report_name.
160+
"""
161+
res = super(Py3oReport, self)._get_report_from_name(report_name)
162+
if res:
163+
return res
164+
# maybe a py3o reprot
165+
report_obj = self.env['ir.actions.report.xml']
166+
return report_obj.search(
167+
[('report_type', '=', 'py3o'),
168+
('report_name', '=', report_name)])
169+
170+
@api.model
171+
def _postprocess_report(self, report_path, res_id, save_in_attachment):
156172
if save_in_attachment.get(res_id):
157-
attachment = {
158-
'name': save_in_attachment.get(res_id),
159-
'datas': base64.encodestring(content),
160-
'datas_fname': save_in_attachment.get(res_id),
161-
'res_model': save_in_attachment.get('model'),
162-
'res_id': res_id,
163-
}
164-
return self.env['ir.attachment'].create(attachment)
165-
return False
173+
with open(report_path, 'rb') as pdfreport:
174+
attachment = {
175+
'name': save_in_attachment.get(res_id),
176+
'datas': base64.encodestring(pdfreport.read()),
177+
'datas_fname': save_in_attachment.get(res_id),
178+
'res_model': save_in_attachment.get('model'),
179+
'res_id': res_id,
180+
}
181+
try:
182+
self.env['ir.attachment'].create(attachment)
183+
except AccessError:
184+
logger.info("Cannot save PDF report %r as attachment",
185+
attachment['name'])
186+
else:
187+
logger.info(
188+
'The PDF document %s is now saved in the database',
189+
attachment['name'])
166190

167191
@api.multi
168192
def _create_single_report(self, model_instance, data, save_in_attachment):
169193
""" This function to generate our py3o report
170194
"""
171195
self.ensure_one()
172196
report_xml = self.ir_actions_report_xml_id
173-
197+
filetype = report_xml.py3o_filetype
198+
result_fd, result_path = tempfile.mkstemp(
199+
suffix='.' + filetype, prefix='p3o.report.tmp.')
174200
tmpl_data = self.get_template()
175201

176202
in_stream = StringIO(tmpl_data)
177-
out_stream = StringIO()
178-
template = Template(in_stream, out_stream, escape_false=True)
179-
localcontext = self._get_parser_context(model_instance, data)
180-
if report_xml.py3o_is_local_fusion:
181-
template.render(localcontext)
182-
in_stream = out_stream
183-
datadict = {}
184-
else:
185-
expressions = template.get_all_user_python_expression()
186-
py_expression = template.convert_py3o_to_python_ast(expressions)
187-
convertor = Py3oConvertor()
188-
data_struct = convertor(py_expression)
189-
datadict = data_struct.render(localcontext)
190-
191-
filetype = report_xml.py3o_filetype
192-
is_native = Formats().get_format(filetype).native
193-
if is_native:
194-
res = out_stream.getvalue()
195-
else: # Call py3o.server to render the template in the desired format
196-
in_stream.seek(0)
203+
with closing(os.fdopen(result_fd, 'w+')) as out_stream:
204+
template = Template(in_stream, out_stream, escape_false=True)
205+
localcontext = self._get_parser_context(model_instance, data)
206+
is_native = Formats().get_format(filetype).native
207+
if report_xml.py3o_is_local_fusion:
208+
template.render(localcontext)
209+
out_stream.seek(0)
210+
in_stream = out_stream.read()
211+
datadict = {}
212+
else:
213+
expressions = template.get_all_user_python_expression()
214+
py_expression = template.convert_py3o_to_python_ast(
215+
expressions)
216+
convertor = Py3oConvertor()
217+
data_struct = convertor(py_expression)
218+
datadict = data_struct.render(localcontext)
219+
220+
if not is_native or not report_xml.py3o_is_local_fusion:
221+
# Call py3o.server to render the template in the desired format
197222
files = {
198223
'tmpl_file': in_stream,
199224
}
@@ -212,21 +237,13 @@ def _create_single_report(self, model_instance, data, save_in_attachment):
212237
_('Fusion server error %s') % r.text,
213238
)
214239

215-
# Here is a little joke about Odoo
216-
# we do nice chunked reading from the network...
217240
chunk_size = 1024
218-
with NamedTemporaryFile(
219-
suffix=filetype,
220-
prefix='py3o-template-'
221-
) as fd:
241+
with open(result_path, 'w+') as fd:
222242
for chunk in r.iter_content(chunk_size):
223243
fd.write(chunk)
224-
fd.seek(0)
225-
# ... but odoo wants the whole data in memory anyways :)
226-
res = fd.read()
227244
self._postprocess_report(
228-
res, model_instance.id, save_in_attachment)
229-
return res, "." + self.ir_actions_report_xml_id.py3o_filetype
245+
result_path, model_instance.id, save_in_attachment)
246+
return result_path
230247

231248
@api.multi
232249
def _get_or_create_single_report(self, model_instance, data,
@@ -241,43 +258,42 @@ def _get_or_create_single_report(self, model_instance, data,
241258
model_instance, data, save_in_attachment)
242259

243260
@api.multi
244-
def _zip_results(self, results):
261+
def _zip_results(self, reports_path):
245262
self.ensure_one()
246263
zfname_prefix = self.ir_actions_report_xml_id.name
247-
with NamedTemporaryFile(suffix="zip", prefix='py3o-zip-result') as fd:
248-
with ZipFile(fd, 'w', ZIP_DEFLATED) as zf:
249-
cpt = 0
250-
for r, ext in results:
251-
fname = "%s_%d.%s" % (zfname_prefix, cpt, ext)
252-
zf.writestr(fname, r)
253-
cpt += 1
254-
fd.seek(0)
255-
return fd.read(), 'zip'
264+
result_path = tempfile.mktemp(suffix="zip", prefix='py3o-zip-result')
265+
with ZipFile(result_path, 'w', ZIP_DEFLATED) as zf:
266+
cpt = 0
267+
for report in reports_path:
268+
fname = "%s_%d.%s" % (
269+
zfname_prefix, cpt, report.split('.')[-1])
270+
zf.write(report, fname)
256271

257-
@api.multi
258-
def _merge_pdfs(self, results):
259-
from pyPdf import PdfFileWriter, PdfFileReader
260-
output = PdfFileWriter()
261-
for r in results:
262-
reader = PdfFileReader(StringIO(r[0]))
263-
for page in range(reader.getNumPages()):
264-
output.addPage(reader.getPage(page))
265-
s = StringIO()
266-
output.write(s)
267-
return s.getvalue(), formats.FORMAT_PDF
272+
cpt += 1
273+
return result_path
268274

269275
@api.multi
270-
def _merge_results(self, results):
276+
def _merge_results(self, reports_path):
271277
self.ensure_one()
272-
if not results:
273-
return False, False
274-
if len(results) == 1:
275-
return results[0]
276278
filetype = self.ir_actions_report_xml_id.py3o_filetype
279+
if not reports_path:
280+
return False, False
281+
if len(reports_path) == 1:
282+
return reports_path[0], filetype
277283
if filetype == formats.FORMAT_PDF:
278-
return self._merge_pdfs(results)
284+
return self._merge_pdf(reports_path), formats.FORMAT_PDF
279285
else:
280-
return self._zip_results(results)
286+
return self._zip_results(reports_path), 'zip'
287+
288+
@api.model
289+
def _cleanup_tempfiles(self, temporary_files):
290+
# Manual cleanup of the temporary files
291+
for temporary_file in temporary_files:
292+
try:
293+
os.unlink(temporary_file)
294+
except (OSError, IOError):
295+
logger.error(
296+
'Error when trying to remove file %s' % temporary_file)
281297

282298
@api.multi
283299
def create_report(self, res_ids, data):
@@ -287,8 +303,21 @@ def create_report(self, res_ids, data):
287303
res_ids)
288304
save_in_attachment = self._check_attachment_use(
289305
model_instances, self.ir_actions_report_xml_id) or {}
290-
results = []
306+
reports_path = []
291307
for model_instance in model_instances:
292-
results.append(self._get_or_create_single_report(
293-
model_instance, data, save_in_attachment))
294-
return self._merge_results(results)
308+
reports_path.append(
309+
self._get_or_create_single_report(
310+
model_instance, data, save_in_attachment))
311+
312+
result_path, filetype = self._merge_results(reports_path)
313+
reports_path.append(result_path)
314+
315+
# Here is a little joke about Odoo
316+
# we do all the generation process using files to avoid memory
317+
# consumption...
318+
# ... but odoo wants the whole data in memory anyways :)
319+
320+
with open(result_path, 'r+b') as fd:
321+
res = fd.read()
322+
self._cleanup_tempfiles(set(reports_path))
323+
return res, filetype

report_py3o/tests/test_report_py3o.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import mock
66
import os
77
import pkg_resources
8+
import tempfile
89

910
from py3o.formats import Formats
1011

@@ -60,11 +61,17 @@ def test_reports(self):
6061
report = self.env.ref("report_py3o.res_users_report_py3o")
6162
with mock.patch.object(
6263
py3o_report.__class__, '_create_single_report') as patched_pdf:
64+
result = tempfile.mktemp('.txt')
65+
with open(result, 'w') as fp:
66+
fp.write('dummy')
67+
patched_pdf.return_value = result
6368
# test the call the the create method inside our custom parser
6469
report.render_report(self.env.user.ids,
6570
report.report_name,
6671
{})
6772
self.assertEqual(1, patched_pdf.call_count)
73+
# generated files no more exists
74+
self.assertFalse(os.path.exists(result))
6875
res = report.render_report(
6976
self.env.user.ids, report.report_name, {})
7077
self.assertTrue(res)

0 commit comments

Comments
 (0)