OCA reporting engine fork for dev and update.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

323 lines
12 KiB

8 years ago
8 years ago
8 years ago
  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. from contextlib import closing
  12. import pkg_resources
  13. import requests
  14. import sys
  15. import tempfile
  16. from zipfile import ZipFile, ZIP_DEFLATED
  17. from odoo.exceptions import AccessError
  18. from odoo.exceptions import UserError
  19. from odoo.report.report_sxw import rml_parse
  20. from odoo import api, fields, models, _
  21. logger = logging.getLogger(__name__)
  22. try:
  23. from py3o.template.helpers import Py3oConvertor
  24. from py3o.template import Template
  25. from py3o import formats
  26. except ImportError:
  27. logger.debug('Cannot import py3o.template')
  28. try:
  29. from py3o.formats import Formats
  30. except ImportError:
  31. logger.debug('Cannot import py3o.formats')
  32. _extender_functions = {}
  33. class TemplateNotFound(Exception):
  34. pass
  35. def py3o_report_extender(report_xml_id=None):
  36. """
  37. A decorator to define function to extend the context sent to a template.
  38. This will be called at the creation of the report.
  39. The following arguments will be passed to it:
  40. - ir_report: report instance
  41. - localcontext: The context that will be passed to the report engine
  42. If no report_xml_id is given the extender is registered for all py3o
  43. reports
  44. Idea copied from CampToCamp report_webkit module.
  45. :param report_xml_id: xml id of the report
  46. :return: a decorated class
  47. """
  48. global _extender_functions
  49. def fct1(fct):
  50. _extender_functions.setdefault(report_xml_id, []).append(fct)
  51. return fct
  52. return fct1
  53. @py3o_report_extender()
  54. def defautl_extend(report_xml, localcontext):
  55. # add the base64decode function to be able do decode binary fields into
  56. # the template
  57. localcontext['b64decode'] = b64decode
  58. localcontext['report_xml'] = report_xml
  59. class Py3oReport(models.TransientModel):
  60. _name = "py3o.report"
  61. _inherit = 'report'
  62. _description = "Report Py30"
  63. ir_actions_report_xml_id = fields.Many2one(
  64. comodel_name="ir.actions.report.xml",
  65. required=True
  66. )
  67. @api.multi
  68. def get_template(self):
  69. """private helper to fetch the template data either from the database
  70. or from the default template file provided by the implementer.
  71. ATM this method takes a report definition recordset
  72. to try and fetch the report template from database. If not found it
  73. will fallback to the template file referenced in the report definition.
  74. @returns: string or buffer containing the template data
  75. @raises: TemplateNotFound which is a subclass of
  76. odoo.exceptions.DeferredException
  77. """
  78. self.ensure_one()
  79. tmpl_data = None
  80. report_xml = self.ir_actions_report_xml_id
  81. if report_xml.py3o_template_id and report_xml.py3o_template_id.id:
  82. # if a user gave a report template
  83. tmpl_data = b64decode(
  84. report_xml.py3o_template_id.py3o_template_data
  85. )
  86. elif report_xml.py3o_template_fallback:
  87. tmpl_name = report_xml.py3o_template_fallback
  88. flbk_filename = None
  89. if report_xml.module:
  90. # if the default is defined
  91. flbk_filename = pkg_resources.resource_filename(
  92. "odoo.addons.%s" % report_xml.module,
  93. tmpl_name,
  94. )
  95. elif os.path.isabs(tmpl_name):
  96. # It is an absolute path
  97. flbk_filename = os.path.normcase(os.path.normpath(tmpl_name))
  98. if flbk_filename and os.path.exists(flbk_filename):
  99. # and it exists on the fileystem
  100. with open(flbk_filename, 'r') as tmpl:
  101. tmpl_data = tmpl.read()
  102. if tmpl_data is None:
  103. # if for any reason the template is not found
  104. raise TemplateNotFound(
  105. _(u'No template found. Aborting.'),
  106. sys.exc_info(),
  107. )
  108. return tmpl_data
  109. @api.multi
  110. def _extend_parser_context(self, context_instance, report_xml):
  111. # add default extenders
  112. for fct in _extender_functions.get(None, []):
  113. fct(report_xml, context_instance.localcontext)
  114. # add extenders for registered on the template
  115. xml_id = report_xml.get_external_id().get(report_xml.id)
  116. if xml_id in _extender_functions:
  117. for fct in _extender_functions[xml_id]:
  118. fct(report_xml, context_instance.localcontext)
  119. @api.multi
  120. def _get_parser_context(self, model_instance, data):
  121. report_xml = self.ir_actions_report_xml_id
  122. context_instance = rml_parse(self.env.cr, self.env.uid,
  123. report_xml.name,
  124. context=self.env.context)
  125. context_instance.set_context(model_instance, data, model_instance.ids,
  126. report_xml.report_type)
  127. self._extend_parser_context(context_instance, report_xml)
  128. return context_instance.localcontext
  129. @api.model
  130. def _get_report_from_name(self, report_name):
  131. """Get the first record of ir.actions.report.xml having the
  132. ``report_name`` as value for the field report_name.
  133. """
  134. res = super(Py3oReport, self)._get_report_from_name(report_name)
  135. if res:
  136. return res
  137. # maybe a py3o reprot
  138. report_obj = self.env['ir.actions.report.xml']
  139. return report_obj.search(
  140. [('report_type', '=', 'py3o'),
  141. ('report_name', '=', report_name)])
  142. @api.model
  143. def _postprocess_report(self, report_path, res_id, save_in_attachment):
  144. if save_in_attachment.get(res_id):
  145. with open(report_path, 'rb') as pdfreport:
  146. attachment = {
  147. 'name': save_in_attachment.get(res_id),
  148. 'datas': base64.encodestring(pdfreport.read()),
  149. 'datas_fname': save_in_attachment.get(res_id),
  150. 'res_model': save_in_attachment.get('model'),
  151. 'res_id': res_id,
  152. }
  153. try:
  154. self.env['ir.attachment'].create(attachment)
  155. except AccessError:
  156. logger.info("Cannot save PDF report %r as attachment",
  157. attachment['name'])
  158. else:
  159. logger.info(
  160. 'The PDF document %s is now saved in the database',
  161. attachment['name'])
  162. @api.multi
  163. def _create_single_report(self, model_instance, data, save_in_attachment):
  164. """ This function to generate our py3o report
  165. """
  166. self.ensure_one()
  167. report_xml = self.ir_actions_report_xml_id
  168. filetype = report_xml.py3o_filetype
  169. result_fd, result_path = tempfile.mkstemp(
  170. suffix='.' + filetype, prefix='p3o.report.tmp.')
  171. tmpl_data = self.get_template()
  172. in_stream = StringIO(tmpl_data)
  173. with closing(os.fdopen(result_fd, 'w+')) as out_stream:
  174. template = Template(in_stream, out_stream, escape_false=True)
  175. localcontext = self._get_parser_context(model_instance, data)
  176. is_native = Formats().get_format(filetype).native
  177. if report_xml.py3o_is_local_fusion:
  178. template.render(localcontext)
  179. out_stream.seek(0)
  180. in_stream = out_stream.read()
  181. datadict = {}
  182. else:
  183. expressions = template.get_all_user_python_expression()
  184. py_expression = template.convert_py3o_to_python_ast(
  185. expressions)
  186. convertor = Py3oConvertor()
  187. data_struct = convertor(py_expression)
  188. datadict = data_struct.render(localcontext)
  189. if not is_native or not report_xml.py3o_is_local_fusion:
  190. # Call py3o.server to render the template in the desired format
  191. files = {
  192. 'tmpl_file': in_stream,
  193. }
  194. fields = {
  195. "targetformat": filetype,
  196. "datadict": json.dumps(datadict),
  197. "image_mapping": "{}",
  198. }
  199. if report_xml.py3o_is_local_fusion:
  200. fields['skipfusion'] = '1'
  201. r = requests.post(
  202. report_xml.py3o_server_id.url, data=fields, files=files)
  203. if r.status_code != 200:
  204. # server says we have an issue... let's tell that to enduser
  205. raise UserError(
  206. _('Fusion server error %s') % r.text,
  207. )
  208. chunk_size = 1024
  209. with open(result_path, 'w+') as fd:
  210. for chunk in r.iter_content(chunk_size):
  211. fd.write(chunk)
  212. self._postprocess_report(
  213. result_path, model_instance.id, save_in_attachment)
  214. return result_path
  215. @api.multi
  216. def _get_or_create_single_report(self, model_instance, data,
  217. save_in_attachment):
  218. self.ensure_one()
  219. if save_in_attachment and save_in_attachment[
  220. 'loaded_documents'].get(model_instance.id):
  221. d = save_in_attachment[
  222. 'loaded_documents'].get(model_instance.id)
  223. return d, self.ir_actions_report_xml_id.py3o_filetype
  224. return self._create_single_report(
  225. model_instance, data, save_in_attachment)
  226. @api.multi
  227. def _zip_results(self, reports_path):
  228. self.ensure_one()
  229. zfname_prefix = self.ir_actions_report_xml_id.name
  230. result_path = tempfile.mktemp(suffix="zip", prefix='py3o-zip-result')
  231. with ZipFile(result_path, 'w', ZIP_DEFLATED) as zf:
  232. cpt = 0
  233. for report in reports_path:
  234. fname = "%s_%d.%s" % (
  235. zfname_prefix, cpt, report.split('.')[-1])
  236. zf.write(report, fname)
  237. cpt += 1
  238. return result_path
  239. @api.multi
  240. def _merge_results(self, reports_path):
  241. self.ensure_one()
  242. filetype = self.ir_actions_report_xml_id.py3o_filetype
  243. if not reports_path:
  244. return False, False
  245. if len(reports_path) == 1:
  246. return reports_path[0], filetype
  247. if filetype == formats.FORMAT_PDF:
  248. return self._merge_pdf(reports_path), formats.FORMAT_PDF
  249. else:
  250. return self._zip_results(reports_path), 'zip'
  251. @api.model
  252. def _cleanup_tempfiles(self, temporary_files):
  253. # Manual cleanup of the temporary files
  254. for temporary_file in temporary_files:
  255. try:
  256. os.unlink(temporary_file)
  257. except (OSError, IOError):
  258. logger.error(
  259. 'Error when trying to remove file %s' % temporary_file)
  260. @api.multi
  261. def create_report(self, res_ids, data):
  262. """ Override this function to handle our py3o report
  263. """
  264. model_instances = self.env[self.ir_actions_report_xml_id.model].browse(
  265. res_ids)
  266. save_in_attachment = self._check_attachment_use(
  267. model_instances, self.ir_actions_report_xml_id) or {}
  268. reports_path = []
  269. for model_instance in model_instances:
  270. reports_path.append(
  271. self._get_or_create_single_report(
  272. model_instance, data, save_in_attachment))
  273. result_path, filetype = self._merge_results(reports_path)
  274. reports_path.append(result_path)
  275. # Here is a little joke about Odoo
  276. # we do all the generation process using files to avoid memory
  277. # consumption...
  278. # ... but odoo wants the whole data in memory anyways :)
  279. with open(result_path, 'r+b') as fd:
  280. res = fd.read()
  281. self._cleanup_tempfiles(set(reports_path))
  282. return res, filetype