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.

342 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 _get_report_from_name(self, report_name):
  148. """Get the first record of ir.actions.report.xml having the
  149. ``report_name`` as value for the field report_name.
  150. """
  151. res = super(Py3oReport, self)._get_report_from_name(report_name)
  152. if res:
  153. return res
  154. # maybe a py3o reprot
  155. report_obj = self.env['ir.actions.report.xml']
  156. return report_obj.search(
  157. [('report_type', '=', 'py3o'),
  158. ('report_name', '=', report_name)])
  159. @api.model
  160. def _postprocess_report(self, report_path, res_id, save_in_attachment):
  161. if save_in_attachment.get(res_id):
  162. with open(report_path, 'rb') as pdfreport:
  163. attachment = {
  164. 'name': save_in_attachment.get(res_id),
  165. 'datas': base64.encodestring(pdfreport.read()),
  166. 'datas_fname': save_in_attachment.get(res_id),
  167. 'res_model': save_in_attachment.get('model'),
  168. 'res_id': res_id,
  169. }
  170. try:
  171. self.env['ir.attachment'].create(attachment)
  172. except AccessError:
  173. logger.info("Cannot save PDF report %r as attachment",
  174. attachment['name'])
  175. else:
  176. logger.info(
  177. 'The PDF document %s is now saved in the database',
  178. attachment['name'])
  179. @api.multi
  180. def _create_single_report(self, model_instance, data, save_in_attachment):
  181. """ This function to generate our py3o report
  182. """
  183. self.ensure_one()
  184. report_xml = self.ir_actions_report_xml_id
  185. filetype = report_xml.py3o_filetype
  186. result_fd, result_path = tempfile.mkstemp(
  187. suffix='.' + filetype, prefix='p3o.report.tmp.')
  188. tmpl_data = self.get_template(model_instance)
  189. in_stream = StringIO(tmpl_data)
  190. with closing(os.fdopen(result_fd, 'w+')) as out_stream:
  191. template = Template(in_stream, out_stream, escape_false=True)
  192. localcontext = self._get_parser_context(model_instance, data)
  193. is_native = Formats().get_format(filetype).native
  194. if report_xml.py3o_is_local_fusion:
  195. template.render(localcontext)
  196. out_stream.seek(0)
  197. in_stream = out_stream.read()
  198. datadict = {}
  199. else:
  200. expressions = template.get_all_user_python_expression()
  201. py_expression = template.convert_py3o_to_python_ast(
  202. expressions)
  203. convertor = Py3oConvertor()
  204. data_struct = convertor(py_expression)
  205. datadict = data_struct.render(localcontext)
  206. if not is_native or not report_xml.py3o_is_local_fusion:
  207. # Call py3o.server to render the template in the desired format
  208. files = {
  209. 'tmpl_file': in_stream,
  210. }
  211. fields = {
  212. "targetformat": filetype,
  213. "datadict": json.dumps(datadict),
  214. "image_mapping": "{}",
  215. }
  216. if report_xml.py3o_is_local_fusion:
  217. fields['skipfusion'] = '1'
  218. r = requests.post(
  219. report_xml.py3o_server_id.url, data=fields, files=files)
  220. if r.status_code != 200:
  221. # server says we have an issue... let's tell that to enduser
  222. raise UserError(
  223. _('Fusion server error %s') % r.text,
  224. )
  225. chunk_size = 1024
  226. with open(result_path, 'w+') as fd:
  227. for chunk in r.iter_content(chunk_size):
  228. fd.write(chunk)
  229. self._postprocess_report(
  230. result_path, model_instance.id, save_in_attachment)
  231. return result_path
  232. @api.multi
  233. def _get_or_create_single_report(self, model_instance, data,
  234. save_in_attachment):
  235. self.ensure_one()
  236. if save_in_attachment and save_in_attachment[
  237. 'loaded_documents'].get(model_instance.id):
  238. d = save_in_attachment[
  239. 'loaded_documents'].get(model_instance.id)
  240. return d, self.ir_actions_report_xml_id.py3o_filetype
  241. return self._create_single_report(
  242. model_instance, data, save_in_attachment)
  243. @api.multi
  244. def _zip_results(self, reports_path):
  245. self.ensure_one()
  246. zfname_prefix = self.ir_actions_report_xml_id.name
  247. result_path = tempfile.mktemp(suffix="zip", prefix='py3o-zip-result')
  248. with ZipFile(result_path, 'w', ZIP_DEFLATED) as zf:
  249. cpt = 0
  250. for report in reports_path:
  251. fname = "%s_%d.%s" % (
  252. zfname_prefix, cpt, report.split('.')[-1])
  253. zf.write(report, fname)
  254. cpt += 1
  255. return result_path
  256. @api.multi
  257. def _merge_results(self, reports_path):
  258. self.ensure_one()
  259. filetype = self.ir_actions_report_xml_id.py3o_filetype
  260. if not reports_path:
  261. return False, False
  262. if len(reports_path) == 1:
  263. return reports_path[0], filetype
  264. if filetype == formats.FORMAT_PDF:
  265. return self._merge_pdf(reports_path), formats.FORMAT_PDF
  266. else:
  267. return self._zip_results(reports_path), 'zip'
  268. @api.model
  269. def _cleanup_tempfiles(self, temporary_files):
  270. # Manual cleanup of the temporary files
  271. for temporary_file in temporary_files:
  272. try:
  273. os.unlink(temporary_file)
  274. except (OSError, IOError):
  275. logger.error(
  276. 'Error when trying to remove file %s' % temporary_file)
  277. @api.multi
  278. def create_report(self, res_ids, data):
  279. """ Override this function to handle our py3o report
  280. """
  281. model_instances = self.env[self.ir_actions_report_xml_id.model].browse(
  282. res_ids)
  283. save_in_attachment = self._check_attachment_use(
  284. model_instances, self.ir_actions_report_xml_id) or {}
  285. reports_path = []
  286. for model_instance in model_instances:
  287. reports_path.append(
  288. self._get_or_create_single_report(
  289. model_instance, data, save_in_attachment))
  290. result_path, filetype = self._merge_results(reports_path)
  291. reports_path.append(result_path)
  292. # Here is a little joke about Odoo
  293. # we do all the generation process using files to avoid memory
  294. # consumption...
  295. # ... but odoo wants the whole data in memory anyways :)
  296. with open(result_path, 'r+b') as fd:
  297. res = fd.read()
  298. self._cleanup_tempfiles(set(reports_path))
  299. return res, filetype