1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # Copyright (c) 2010 Camptocamp SA (http://www.camptocamp.com)
7 # Author : Nicolas Bessi (Camptocamp)
8 # Contributor(s) : Florent Xicluna (Wingo SA)
10 # WARNING: This program as such is intended to be used by professional
11 # programmers who take the whole responsability of assessing all potential
12 # consequences resulting from its eventual inadequacies and bugs
13 # End users who are looking for a ready-to-use solution with commercial
14 # garantees and support are strongly adviced to contract a Free Software
17 # This program is Free Software; you can redistribute it and/or
18 # modify it under the terms of the GNU General Public License
19 # as published by the Free Software Foundation; either version 2
20 # of the License, or (at your option) any later version.
22 # This program is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 # GNU General Public License for more details.
27 # You should have received a copy of the GNU General Public License
28 # along with this program; if not, write to the Free Software
29 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
31 ##############################################################################
36 from openerp import report
40 from functools import partial
42 from report_helper import WebKitHelper
44 from openerp.modules.module import get_module_resource
45 from openerp.report.report_sxw import *
46 from openerp import tools
47 from openerp.tools.translate import _
48 from openerp.osv.osv import except_osv
49 from urllib import urlencode, quote as quote
51 _logger = logging.getLogger(__name__)
54 # We use a jinja2 sandboxed environment to render mako templates.
55 # Note that the rendering does not cover all the mako syntax, in particular
56 # arbitrary Python statements are not accepted, and not all expressions are
57 # allowed: only "public" attributes (not starting with '_') of objects may
59 # This is done on purpose: it prevents incidental or malicious execution of
60 # Python code that may break the security of the server.
61 from jinja2.sandbox import SandboxedEnvironment
62 mako_template_env = SandboxedEnvironment(
63 block_start_string="<%",
64 block_end_string="%>",
65 variable_start_string="${",
66 variable_end_string="}",
67 comment_start_string="<%doc>",
68 comment_end_string="</%doc>",
69 line_statement_prefix="%",
70 line_comment_prefix="##",
71 trim_blocks=True, # do not output newline after blocks
72 autoescape=True, # XML/HTML automatic escaping
74 mako_template_env.globals.update({
77 'urlencode': urlencode,
80 _logger.warning("jinja2 not available, templating features will not work!")
82 def mako_template(text):
83 """Build a Mako template.
85 This template uses UTF-8 encoding
88 return mako_template_env.from_string(text)
90 _extender_functions = {}
92 def webkit_report_extender(report_name):
94 A decorator to define functions to extend the context used in a template rendering.
95 report_name must be the xml id of the desired report (it is mandatory to indicate the
96 module in that xml id).
98 The given function will be called at the creation of the report. The following arguments
99 will be passed to it (in this order):
100 - pool The model pool.
103 - localcontext The context given to the template engine to render the templates for the
104 current report. This is the context that should be modified.
105 - context The OpenERP context.
108 lst = _extender_functions.get(report_name)
111 _extender_functions[report_name] = lst
117 class WebKitParser(report_sxw):
118 """Custom class that use webkit to render HTML reports
119 Code partially taken from report openoffice. Thanks guys :)
121 def __init__(self, name, table, rml=False, parser=rml_parse,
122 header=True, store=False, register=True):
123 self.localcontext = {}
124 report_sxw.__init__(self, name, table, rml, parser,
125 header, store, register=register)
127 def get_lib(self, cursor, uid):
128 """Return the lib wkhtml path"""
129 proxy = self.pool['ir.config_parameter']
130 webkit_path = proxy.get_param(cursor, uid, 'webkit_path')
134 defpath = os.environ.get('PATH', os.defpath).split(os.pathsep)
135 if hasattr(sys, 'frozen'):
136 defpath.append(os.getcwd())
137 if tools.config['root_path']:
138 defpath.append(os.path.dirname(tools.config['root_path']))
139 webkit_path = tools.which('wkhtmltopdf', path=os.pathsep.join(defpath))
147 _('Wkhtmltopdf library path is not set'),
148 _('Please install executable on your system' \
149 ' (sudo apt-get install wkhtmltopdf) or download it from here:' \
150 ' http://code.google.com/p/wkhtmltopdf/downloads/list and set the' \
151 ' path in the ir.config_parameter with the webkit_path key.' \
152 'Minimal version is 0.9.9')
155 def generate_pdf(self, comm_path, report_xml, header, footer, html_list, webkit_header=False):
156 """Call webkit in order to generate pdf"""
157 if not webkit_header:
158 webkit_header = report_xml.webkit_header
159 fd, out_filename = tempfile.mkstemp(suffix=".pdf",
160 prefix="webkit.tmp.")
161 file_to_del = [out_filename]
163 command = [comm_path]
165 command = ['wkhtmltopdf']
167 command.append('--quiet')
168 # default to UTF-8 encoding. Use <meta charset="latin-1"> to override.
169 command.extend(['--encoding', 'utf-8'])
171 with tempfile.NamedTemporaryFile(suffix=".head.html",
172 delete=False) as head_file:
173 head_file.write(self._sanitize_html(header.encode('utf-8')))
174 file_to_del.append(head_file.name)
175 command.extend(['--header-html', head_file.name])
177 with tempfile.NamedTemporaryFile(suffix=".foot.html",
178 delete=False) as foot_file:
179 foot_file.write(self._sanitize_html(footer.encode('utf-8')))
180 file_to_del.append(foot_file.name)
181 command.extend(['--footer-html', foot_file.name])
183 if webkit_header.margin_top :
184 command.extend(['--margin-top', str(webkit_header.margin_top).replace(',', '.')])
185 if webkit_header.margin_bottom :
186 command.extend(['--margin-bottom', str(webkit_header.margin_bottom).replace(',', '.')])
187 if webkit_header.margin_left :
188 command.extend(['--margin-left', str(webkit_header.margin_left).replace(',', '.')])
189 if webkit_header.margin_right :
190 command.extend(['--margin-right', str(webkit_header.margin_right).replace(',', '.')])
191 if webkit_header.orientation :
192 command.extend(['--orientation', str(webkit_header.orientation).replace(',', '.')])
193 if webkit_header.format :
194 command.extend(['--page-size', str(webkit_header.format).replace(',', '.')])
196 for html in html_list :
197 with tempfile.NamedTemporaryFile(suffix="%d.body.html" %count,
198 delete=False) as html_file:
200 html_file.write(self._sanitize_html(html.encode('utf-8')))
201 file_to_del.append(html_file.name)
202 command.append(html_file.name)
203 command.append(out_filename)
204 stderr_fd, stderr_path = tempfile.mkstemp(text=True)
205 file_to_del.append(stderr_path)
207 status = subprocess.call(command, stderr=stderr_fd)
208 os.close(stderr_fd) # ensure flush before reading
209 stderr_fd = None # avoid closing again in finally block
210 fobj = open(stderr_path, 'r')
211 error_message = fobj.read()
213 if not error_message:
214 error_message = _('No diagnosis message was provided')
216 error_message = _('The following diagnosis message was provided:\n') + error_message
218 raise except_osv(_('Webkit error' ),
219 _("The command 'wkhtmltopdf' failed with error code = %s. Message: %s") % (status, error_message))
220 with open(out_filename, 'rb') as pdf_file:
221 pdf = pdf_file.read()
224 if stderr_fd is not None:
226 for f_to_del in file_to_del:
229 except (OSError, IOError), exc:
230 _logger.error('cannot remove file %s: %s', f_to_del, exc)
233 def translate_call(self, parser_instance, src):
234 """Translate String."""
235 ir_translation = self.pool['ir.translation']
236 name = self.tmpl and 'addons/' + self.tmpl or None
237 res = ir_translation._get_source(parser_instance.cr, parser_instance.uid,
238 name, 'report', parser_instance.localcontext.get('lang', 'en_US'), src)
240 # no translation defined, fallback on None (backward compatibility)
241 res = ir_translation._get_source(parser_instance.cr, parser_instance.uid,
242 None, 'report', parser_instance.localcontext.get('lang', 'en_US'), src)
247 # override needed to keep the attachments storing procedure
248 def create_single_pdf(self, cursor, uid, ids, data, report_xml, context=None):
249 """generate the PDF"""
251 # just try to find an xml id for the report
253 pool = openerp.registry(cr.dbname)
254 found_xml_ids = pool["ir.model.data"].search(cr, uid, [["model", "=", "ir.actions.report.xml"], \
255 ["res_id", "=", report_xml.id]], context=context)
258 xml_id = pool["ir.model.data"].read(cr, uid, found_xml_ids[0], ["module", "name"])
259 xml_id = "%s.%s" % (xml_id["module"], xml_id["name"])
264 if report_xml.report_type != 'webkit':
265 return super(WebKitParser,self).create_single_pdf(cursor, uid, ids, data, report_xml, context=context)
267 parser_instance = self.parser(cursor,
273 objs = self.getObjects(cursor, uid, ids, context)
274 parser_instance.set_context(objs, data, ids, report_xml.report_type)
278 if report_xml.report_file :
279 path = get_module_resource(*report_xml.report_file.split('/'))
280 if path and os.path.exists(path) :
281 template = file(path).read()
282 if not template and report_xml.report_webkit_data :
283 template = report_xml.report_webkit_data
285 raise except_osv(_('Error!'), _('Webkit report template not found!'))
286 header = report_xml.webkit_header.html
287 footer = report_xml.webkit_header.footer_html
288 if not header and report_xml.header:
290 _('No header defined for this Webkit report!'),
291 _('Please set a header in company settings.')
293 if not report_xml.header :
295 default_head = get_module_resource('report_webkit', 'default_header.html')
296 with open(default_head,'r') as f:
298 css = report_xml.webkit_header.css
302 translate_call = partial(self.translate_call, parser_instance)
303 body_mako_tpl = mako_template(template)
304 helper = WebKitHelper(cursor, uid, report_xml.id, context)
305 parser_instance.localcontext['helper'] = helper
306 parser_instance.localcontext['css'] = css
307 parser_instance.localcontext['_'] = translate_call
309 # apply extender functions
311 if xml_id in _extender_functions:
312 for fct in _extender_functions[xml_id]:
313 fct(pool, cr, uid, parser_instance.localcontext, context)
315 if report_xml.precise_mode:
316 ctx = dict(parser_instance.localcontext)
317 for obj in parser_instance.localcontext['objects']:
318 ctx['objects'] = [obj]
320 html = body_mako_tpl.render(dict(ctx))
325 raise except_osv(_('Webkit render!'), msg)
328 html = body_mako_tpl.render(dict(parser_instance.localcontext))
333 raise except_osv(_('Webkit render!'), msg)
334 head_mako_tpl = mako_template(header)
336 head = head_mako_tpl.render(dict(parser_instance.localcontext, _debug=False))
338 raise except_osv(_('Webkit render!'), u"%s" % e)
341 foot_mako_tpl = mako_template(footer)
343 foot = foot_mako_tpl.render(dict(parser_instance.localcontext))
347 raise except_osv(_('Webkit render!'), msg)
348 if report_xml.webkit_debug :
350 deb = head_mako_tpl.render(dict(parser_instance.localcontext, _debug=tools.ustr("\n".join(htmls))))
354 raise except_osv(_('Webkit render!'), msg)
356 bin = self.get_lib(cursor, uid)
357 pdf = self.generate_pdf(bin, report_xml, head, foot, htmls)
360 def create(self, cursor, uid, ids, data, context=None):
361 """We override the create function in order to handle generator
362 Code taken from report openoffice. Thanks guys :) """
363 pool = openerp.registry(cursor.dbname)
364 ir_obj = pool['ir.actions.report.xml']
365 report_xml_ids = ir_obj.search(cursor, uid,
366 [('report_name', '=', self.name[7:])], context=context)
369 report_xml = ir_obj.browse(cursor,
373 report_xml.report_rml = None
374 report_xml.report_rml_content = None
375 report_xml.report_sxw_content_data = None
376 report_xml.report_sxw_content = None
377 report_xml.report_sxw = None
379 return super(WebKitParser, self).create(cursor, uid, ids, data, context)
380 if report_xml.report_type != 'webkit':
381 return super(WebKitParser, self).create(cursor, uid, ids, data, context)
382 result = self.create_source_pdf(cursor, uid, ids, data, report_xml, context)
387 def _sanitize_html(self, html):
388 """wkhtmltopdf expects the html page to declare a doctype.
390 if html and html[:9].upper() != "<!DOCTYPE":
391 html = "<!DOCTYPE html>\n" + html
394 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: