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.

227 lines
7.8 KiB

  1. # -*- coding: utf-8 -*-
  2. # Copyright 2013 XCG Consulting (http://odoo.consulting)
  3. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
  4. from cStringIO import StringIO
  5. import json
  6. import pkg_resources
  7. import os
  8. import sys
  9. from base64 import b64decode
  10. import requests
  11. from tempfile import NamedTemporaryFile
  12. from py3o.template.helpers import Py3oConvertor
  13. from py3o.template import Template
  14. from py3o.formats import Formats
  15. from openerp import _
  16. from openerp import exceptions
  17. from openerp.report.report_sxw import report_sxw, rml_parse
  18. from openerp import registry
  19. _extender_functions = {}
  20. class TemplateNotFound(Exception):
  21. pass
  22. def py3o_report_extender(report_name):
  23. """
  24. A decorator to define function to extend the context sent to a template.
  25. This will be called at the creation of the report.
  26. The following arguments will be passed to it:
  27. - pool: the model pool
  28. - cr: the database cursor
  29. - uid: the id of the user that call the renderer
  30. - localcontext: The context that will be passed to the report engine
  31. - context: the Odoo context
  32. Method copied from CampToCamp report_webkit module.
  33. :param report_name: xml id of the report
  34. :return: a decorated class
  35. """
  36. def fct1(fct):
  37. lst = _extender_functions.get(report_name)
  38. if not lst:
  39. lst = []
  40. _extender_functions[report_name] = lst
  41. lst.append(fct)
  42. return fct
  43. return fct1
  44. class Py3oParser(report_sxw):
  45. """Custom class that use Py3o to render libroffice reports.
  46. Code partially taken from CampToCamp's webkit_report."""
  47. def __init__(self, name, table, rml=False, parser=rml_parse,
  48. header=False, store=False, register=True):
  49. self.localcontext = {}
  50. super(Py3oParser, self).__init__(
  51. name, table, rml=rml, parser=parser,
  52. header=header, store=store, register=register
  53. )
  54. def get_template(self, report_obj):
  55. """private helper to fetch the template data either from the database
  56. or from the default template file provided by the implementer.
  57. ATM this method takes a report definition recordset
  58. to try and fetch the report template from database. If not found it
  59. will fallback to the template file referenced in the report definition.
  60. @param report_obj: a recordset representing the report defintion
  61. @type report_obj: openerp.model.recordset instance
  62. @returns: string or buffer containing the template data
  63. @raises: TemplateNotFound which is a subclass of
  64. openerp.exceptions.DeferredException
  65. """
  66. tmpl_data = None
  67. if report_obj.py3o_template_id and report_obj.py3o_template_id.id:
  68. # if a user gave a report template
  69. tmpl_data = b64decode(
  70. report_obj.py3o_template_id.py3o_template_data
  71. )
  72. elif report_obj.py3o_template_fallback:
  73. tmpl_name = report_obj.py3o_template_fallback
  74. flbk_filename = None
  75. if report_obj.module:
  76. # if the default is defined
  77. flbk_filename = pkg_resources.resource_filename(
  78. "openerp.addons.%s" % report_obj.module,
  79. tmpl_name,
  80. )
  81. elif os.path.isabs(tmpl_name):
  82. # It is an absolute path
  83. flbk_filename = os.path.normcase(os.path.normpath(tmpl_name))
  84. if flbk_filename and os.path.exists(flbk_filename):
  85. # and it exists on the fileystem
  86. with open(flbk_filename, 'r') as tmpl:
  87. tmpl_data = tmpl.read()
  88. if tmpl_data is None:
  89. # if for any reason the template is not found
  90. raise TemplateNotFound(
  91. _(u'No template found. Aborting.'),
  92. sys.exc_info(),
  93. )
  94. return tmpl_data
  95. def create_single_pdf(self, cr, uid, ids, data, report_xml, context=None):
  96. """ Overide this function to generate our py3o report
  97. """
  98. if report_xml.report_type != 'py3o':
  99. return super(Py3oParser, self).create_single_pdf(
  100. cr, uid, ids, data, report_xml, context=context
  101. )
  102. pool = registry(cr.dbname)
  103. model_data_ids = pool['ir.model.data'].search(
  104. cr, uid, [
  105. ('model', '=', 'ir.actions.report.xml'),
  106. ('res_id', '=', report_xml.id),
  107. ]
  108. )
  109. xml_id = None
  110. if model_data_ids:
  111. model_data = pool['ir.model.data'].browse(
  112. cr, uid, model_data_ids[0], context=context
  113. )
  114. xml_id = '%s.%s' % (model_data.module, model_data.name)
  115. parser_instance = self.parser(cr, uid, self.name2, context=context)
  116. parser_instance.set_context(
  117. self.getObjects(cr, uid, ids, context),
  118. data, ids, report_xml.report_type
  119. )
  120. if xml_id in _extender_functions:
  121. for fct in _extender_functions[xml_id]:
  122. fct(pool, cr, uid, parser_instance.localcontext, context)
  123. tmpl_data = self.get_template(report_xml)
  124. in_stream = StringIO(tmpl_data)
  125. out_stream = StringIO()
  126. template = Template(in_stream, out_stream)
  127. localcontext = parser_instance.localcontext
  128. if report_xml.py3o_is_local_fusion:
  129. template.render(localcontext)
  130. input = out_stream.getvalue()
  131. else:
  132. expressions = template.get_all_user_python_expression()
  133. py_expression = template.convert_py3o_to_python_ast(expressions)
  134. convertor = Py3oConvertor()
  135. data_struct = convertor(py_expression)
  136. input = data_struct.render(localcontext)
  137. filetype = report_xml.py3o_fusion_filetype
  138. is_native = Formats().get_format(filetype)
  139. if is_native:
  140. res = input
  141. else: # Call py3o.server to render the template in the desired format
  142. in_stream.seek(0)
  143. files = {
  144. 'tmpl_file': in_stream,
  145. }
  146. fields = {
  147. "targetformat": filetype.fusion_ext,
  148. "datadict": json.dumps(input),
  149. "image_mapping": "{}",
  150. }
  151. r = requests.post(
  152. report_xml.py3o_server_id.url, data=fields, files=files)
  153. if r.status_code != 200:
  154. # server says we have an issue... let's tell that to enduser
  155. raise exceptions.Warning(
  156. _('Fusion server error %s') % r.text,
  157. )
  158. # Here is a little joke about Odoo
  159. # we do nice chunked reading from the network...
  160. chunk_size = 1024
  161. with NamedTemporaryFile(
  162. suffix=filetype.human_ext,
  163. prefix='py3o-template-'
  164. ) as fd:
  165. for chunk in r.iter_content(chunk_size):
  166. fd.write(chunk)
  167. fd.seek(0)
  168. # ... but odoo wants the whole data in memory anyways :)
  169. res = fd.read()
  170. return res, "." + filetype
  171. def create(self, cr, uid, ids, data, context=None):
  172. """ Override this function to handle our py3o report
  173. """
  174. pool = registry(cr.dbname)
  175. ir_action_report_obj = pool['ir.actions.report.xml']
  176. report_xml_ids = ir_action_report_obj.search(
  177. cr, uid, [('report_name', '=', self.name[7:])], context=context
  178. )
  179. if not report_xml_ids:
  180. return super(Py3oParser, self).create(
  181. cr, uid, ids, data, context=context
  182. )
  183. report_xml = ir_action_report_obj.browse(
  184. cr, uid, report_xml_ids[0], context=context
  185. )
  186. result = self.create_source_pdf(
  187. cr, uid, ids, data, report_xml, context
  188. )
  189. if not result:
  190. return False, False
  191. return result