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.

219 lines
7.6 KiB

8 years ago
8 years ago
8 years ago
  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 odoo import api, _
  13. from odoo import exceptions
  14. from odoo.report.report_sxw import report_sxw
  15. import logging
  16. logger = logging.getLogger(__name__)
  17. try:
  18. from py3o.template.helpers import Py3oConvertor
  19. from py3o.template import Template
  20. except ImportError:
  21. logger.debug('Cannot import py3o.template')
  22. try:
  23. from py3o.formats import Formats
  24. except ImportError:
  25. logger.debug('Cannot import py3o.formats')
  26. _extender_functions = {}
  27. class TemplateNotFound(Exception):
  28. pass
  29. def py3o_report_extender(report_xml_id=None):
  30. """
  31. A decorator to define function to extend the context sent to a template.
  32. This will be called at the creation of the report.
  33. The following arguments will be passed to it:
  34. - ir_report: report instance
  35. - localcontext: The context that will be passed to the report engine
  36. If no report_xml_id is given the extender is registered for all py3o
  37. reports
  38. Idea copied from CampToCamp report_webkit module.
  39. :param report_xml_id: xml id of the report
  40. :return: a decorated class
  41. """
  42. global _extender_functions
  43. def fct1(fct):
  44. _extender_functions.setdefault(report_xml_id, []).append(fct)
  45. return fct
  46. return fct1
  47. @py3o_report_extender()
  48. def defautl_extend(report_xml, localcontext):
  49. # add the base64decode function to be able do decode binary fields into
  50. # the template
  51. localcontext['b64decode'] = b64decode
  52. class Py3oParser(report_sxw):
  53. """Custom class that use Py3o to render libroffice reports.
  54. Code partially taken from CampToCamp's webkit_report."""
  55. def get_template(self, report_obj):
  56. """private helper to fetch the template data either from the database
  57. or from the default template file provided by the implementer.
  58. ATM this method takes a report definition recordset
  59. to try and fetch the report template from database. If not found it
  60. will fallback to the template file referenced in the report definition.
  61. @param report_obj: a recordset representing the report defintion
  62. @type report_obj: odoo.model.recordset instance
  63. @returns: string or buffer containing the template data
  64. @raises: TemplateNotFound which is a subclass of
  65. odoo.exceptions.DeferredException
  66. """
  67. tmpl_data = None
  68. if report_obj.py3o_template_id and report_obj.py3o_template_id.id:
  69. # if a user gave a report template
  70. tmpl_data = b64decode(
  71. report_obj.py3o_template_id.py3o_template_data
  72. )
  73. elif report_obj.py3o_template_fallback:
  74. tmpl_name = report_obj.py3o_template_fallback
  75. flbk_filename = None
  76. if report_obj.module:
  77. # if the default is defined
  78. flbk_filename = pkg_resources.resource_filename(
  79. "odoo.addons.%s" % report_obj.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. tmpl_data = tmpl.read()
  89. if tmpl_data is None:
  90. # if for any reason the template is not found
  91. raise TemplateNotFound(
  92. _(u'No template found. Aborting.'),
  93. sys.exc_info(),
  94. )
  95. return tmpl_data
  96. def _extend_parser_context(self, parser_instance, report_xml):
  97. # add default extenders
  98. for fct in _extender_functions.get(None, []):
  99. fct(report_xml, parser_instance.localcontext)
  100. # add extenders for registered on the template
  101. xml_id = report_xml.get_external_id().get(report_xml.id)
  102. if xml_id in _extender_functions:
  103. for fct in _extender_functions[xml_id]:
  104. fct(report_xml, parser_instance.localcontext)
  105. def create_single_pdf(self, cr, uid, ids, data, report_xml, context=None):
  106. """ Overide this function to generate our py3o report
  107. """
  108. if report_xml.report_type != 'py3o':
  109. return super(Py3oParser, self).create_single_pdf(
  110. cr, uid, ids, data, report_xml, context=context
  111. )
  112. parser_instance = self.parser(cr, uid, self.name2, context=context)
  113. parser_instance.set_context(
  114. self.getObjects(cr, uid, ids, context),
  115. data, ids, report_xml.report_type
  116. )
  117. self._extend_parser_context(parser_instance, report_xml)
  118. tmpl_data = self.get_template(report_xml)
  119. in_stream = StringIO(tmpl_data)
  120. out_stream = StringIO()
  121. template = Template(in_stream, out_stream)
  122. localcontext = parser_instance.localcontext
  123. if report_xml.py3o_is_local_fusion:
  124. template.render(localcontext)
  125. in_stream = out_stream
  126. datadict = {}
  127. else:
  128. expressions = template.get_all_user_python_expression()
  129. py_expression = template.convert_py3o_to_python_ast(expressions)
  130. convertor = Py3oConvertor()
  131. data_struct = convertor(py_expression)
  132. datadict = data_struct.render(localcontext)
  133. filetype = report_xml.py3o_filetype
  134. is_native = Formats().get_format(filetype).native
  135. if is_native:
  136. res = out_stream.getvalue()
  137. else: # Call py3o.server to render the template in the desired format
  138. in_stream.seek(0)
  139. files = {
  140. 'tmpl_file': in_stream,
  141. }
  142. fields = {
  143. "targetformat": filetype,
  144. "datadict": json.dumps(datadict),
  145. "image_mapping": "{}",
  146. }
  147. if report_xml.py3o_is_local_fusion:
  148. fields['skipfusion'] = '1'
  149. r = requests.post(
  150. report_xml.py3o_server_id.url, data=fields, files=files)
  151. if r.status_code != 200:
  152. # server says we have an issue... let's tell that to enduser
  153. raise exceptions.Warning(
  154. _('Fusion server error %s') % r.text,
  155. )
  156. # Here is a little joke about Odoo
  157. # we do nice chunked reading from the network...
  158. chunk_size = 1024
  159. with NamedTemporaryFile(
  160. suffix=filetype,
  161. prefix='py3o-template-'
  162. ) as fd:
  163. for chunk in r.iter_content(chunk_size):
  164. fd.write(chunk)
  165. fd.seek(0)
  166. # ... but odoo wants the whole data in memory anyways :)
  167. res = fd.read()
  168. return res, "." + filetype
  169. def create(self, cr, uid, ids, data, context=None):
  170. """ Override this function to handle our py3o report
  171. """
  172. env = api.Environment(cr, uid, context)
  173. report_xmls = env['ir.actions.report.xml'].search(
  174. [('report_name', '=', self.name[7:])])
  175. if not report_xmls:
  176. return super(Py3oParser, self).create(
  177. cr, uid, ids, data, context=context
  178. )
  179. result = self.create_source_pdf(
  180. cr, uid, ids, data, report_xmls[0], context
  181. )
  182. if not result:
  183. return False, False
  184. return result