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.

328 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_from_path(self, tmpl_name):
  69. """"Return the template from the path to root of the module if specied
  70. or an absolute path on your server
  71. """
  72. if not tmpl_name:
  73. return None
  74. report_xml = self.ir_actions_report_xml_id
  75. flbk_filename = None
  76. if report_xml.module:
  77. # if the default is defined
  78. flbk_filename = pkg_resources.resource_filename(
  79. "odoo.addons.%s" % report_xml.module,
  80. tmpl_name,
  81. )
  82. elif os.path.isabs(tmpl_name):
  83. # It is an absolute path
  84. flbk_filename = os.path.normcase(os.path.normpath(tmpl_name))
  85. if flbk_filename and os.path.exists(flbk_filename):
  86. # and it exists on the fileystem
  87. with open(flbk_filename, 'r') as tmpl:
  88. return tmpl.read()
  89. return None
  90. @api.multi
  91. def _get_template_fallback(self, model_instance):
  92. """
  93. Return the template referenced in the report definition
  94. :return:
  95. """
  96. self.ensure_one()
  97. report_xml = self.ir_actions_report_xml_id
  98. return self._get_template_from_path(report_xml.py3o_template_fallback)
  99. @api.multi
  100. def get_template(self, model_instance):
  101. """private helper to fetch the template data either from the database
  102. or from the default template file provided by the implementer.
  103. ATM this method takes a report definition recordset
  104. to try and fetch the report template from database. If not found it
  105. will fallback to the template file referenced in the report definition.
  106. @returns: string or buffer containing the template data
  107. @raises: TemplateNotFound which is a subclass of
  108. odoo.exceptions.DeferredException
  109. """
  110. self.ensure_one()
  111. report_xml = self.ir_actions_report_xml_id
  112. if report_xml.py3o_template_id and report_xml.py3o_template_id.id:
  113. # if a user gave a report template
  114. tmpl_data = b64decode(
  115. report_xml.py3o_template_id.py3o_template_data
  116. )
  117. else:
  118. tmpl_data = self._get_template_fallback(model_instance)
  119. if tmpl_data is None:
  120. # if for any reason the template is not found
  121. raise TemplateNotFound(
  122. _(u'No template found. Aborting.'),
  123. sys.exc_info(),
  124. )
  125. return tmpl_data
  126. @api.multi
  127. def _extend_parser_context(self, context_instance, report_xml):
  128. # add default extenders
  129. for fct in _extender_functions.get(None, []):
  130. fct(report_xml, context_instance.localcontext)
  131. # add extenders for registered on the template
  132. xml_id = report_xml.get_external_id().get(report_xml.id)
  133. if xml_id in _extender_functions:
  134. for fct in _extender_functions[xml_id]:
  135. fct(report_xml, context_instance.localcontext)
  136. @api.multi
  137. def _get_parser_context(self, model_instance, data):
  138. report_xml = self.ir_actions_report_xml_id
  139. context_instance = rml_parse(self.env.cr, self.env.uid,
  140. report_xml.name,
  141. context=self.env.context)
  142. context_instance.set_context(model_instance, data, model_instance.ids,
  143. report_xml.report_type)
  144. self._extend_parser_context(context_instance, report_xml)
  145. return context_instance.localcontext
  146. @api.model
  147. def _postprocess_report(self, report_path, res_id, save_in_attachment):
  148. if save_in_attachment.get(res_id):
  149. with open(report_path, 'rb') as pdfreport:
  150. attachment = {
  151. 'name': save_in_attachment.get(res_id),
  152. 'datas': base64.encodestring(pdfreport.read()),
  153. 'datas_fname': save_in_attachment.get(res_id),
  154. 'res_model': save_in_attachment.get('model'),
  155. 'res_id': res_id,
  156. }
  157. try:
  158. self.env['ir.attachment'].create(attachment)
  159. except AccessError:
  160. logger.info("Cannot save PDF report %r as attachment",
  161. attachment['name'])
  162. else:
  163. logger.info(
  164. 'The PDF document %s is now saved in the database',
  165. attachment['name'])
  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. filetype = report_xml.py3o_filetype
  173. result_fd, result_path = tempfile.mkstemp(
  174. suffix='.' + filetype, prefix='p3o.report.tmp.')
  175. tmpl_data = self.get_template(model_instance)
  176. in_stream = StringIO(tmpl_data)
  177. with closing(os.fdopen(result_fd, 'w+')) as out_stream:
  178. template = Template(in_stream, out_stream, escape_false=True)
  179. localcontext = self._get_parser_context(model_instance, data)
  180. is_native = Formats().get_format(filetype).native
  181. if report_xml.py3o_is_local_fusion:
  182. template.render(localcontext)
  183. out_stream.seek(0)
  184. in_stream = out_stream.read()
  185. datadict = {}
  186. else:
  187. expressions = template.get_all_user_python_expression()
  188. py_expression = template.convert_py3o_to_python_ast(
  189. expressions)
  190. convertor = Py3oConvertor()
  191. data_struct = convertor(py_expression)
  192. datadict = data_struct.render(localcontext)
  193. if not is_native or not report_xml.py3o_is_local_fusion:
  194. # Call py3o.server to render the template in the desired format
  195. files = {
  196. 'tmpl_file': in_stream,
  197. }
  198. fields = {
  199. "targetformat": filetype,
  200. "datadict": json.dumps(datadict),
  201. "image_mapping": "{}",
  202. }
  203. if report_xml.py3o_is_local_fusion:
  204. fields['skipfusion'] = '1'
  205. r = requests.post(
  206. report_xml.py3o_server_id.url, data=fields, files=files)
  207. if r.status_code != 200:
  208. # server says we have an issue... let's tell that to enduser
  209. raise UserError(
  210. _('Fusion server error %s') % r.text,
  211. )
  212. chunk_size = 1024
  213. with open(result_path, 'w+') as fd:
  214. for chunk in r.iter_content(chunk_size):
  215. fd.write(chunk)
  216. self._postprocess_report(
  217. result_path, model_instance.id, save_in_attachment)
  218. return result_path
  219. @api.multi
  220. def _get_or_create_single_report(self, model_instance, data,
  221. save_in_attachment):
  222. self.ensure_one()
  223. if save_in_attachment and save_in_attachment[
  224. 'loaded_documents'].get(model_instance.id):
  225. d = save_in_attachment[
  226. 'loaded_documents'].get(model_instance.id)
  227. return d, self.ir_actions_report_xml_id.py3o_filetype
  228. return self._create_single_report(
  229. model_instance, data, save_in_attachment)
  230. @api.multi
  231. def _zip_results(self, reports_path):
  232. self.ensure_one()
  233. zfname_prefix = self.ir_actions_report_xml_id.name
  234. result_path = tempfile.mktemp(suffix="zip", prefix='py3o-zip-result')
  235. with ZipFile(result_path, 'w', ZIP_DEFLATED) as zf:
  236. cpt = 0
  237. for report in reports_path:
  238. fname = "%s_%d.%s" % (
  239. zfname_prefix, cpt, report.split('.')[-1])
  240. zf.write(report, fname)
  241. cpt += 1
  242. return result_path
  243. @api.multi
  244. def _merge_results(self, reports_path):
  245. self.ensure_one()
  246. filetype = self.ir_actions_report_xml_id.py3o_filetype
  247. if not reports_path:
  248. return False, False
  249. if len(reports_path) == 1:
  250. return reports_path[0], filetype
  251. if filetype == formats.FORMAT_PDF:
  252. return self._merge_pdf(reports_path), formats.FORMAT_PDF
  253. else:
  254. return self._zip_results(reports_path), 'zip'
  255. @api.model
  256. def _cleanup_tempfiles(self, temporary_files):
  257. # Manual cleanup of the temporary files
  258. for temporary_file in temporary_files:
  259. try:
  260. os.unlink(temporary_file)
  261. except (OSError, IOError):
  262. logger.error(
  263. 'Error when trying to remove file %s' % temporary_file)
  264. @api.multi
  265. def create_report(self, res_ids, data):
  266. """ Override this function to handle our py3o report
  267. """
  268. model_instances = self.env[self.ir_actions_report_xml_id.model].browse(
  269. res_ids)
  270. save_in_attachment = self._check_attachment_use(
  271. res_ids, self.ir_actions_report_xml_id) or {}
  272. reports_path = []
  273. for model_instance in model_instances:
  274. reports_path.append(
  275. self._get_or_create_single_report(
  276. model_instance, data, save_in_attachment))
  277. result_path, filetype = self._merge_results(reports_path)
  278. reports_path.append(result_path)
  279. # Here is a little joke about Odoo
  280. # we do all the generation process using files to avoid memory
  281. # consumption...
  282. # ... but odoo wants the whole data in memory anyways :)
  283. with open(result_path, 'r+b') as fd:
  284. res = fd.read()
  285. self._cleanup_tempfiles(set(reports_path))
  286. return res, filetype