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
41 from report_helper import WebKitHelper
43 from openerp.modules.module import get_module_resource
44 from openerp.report.report_sxw import *
45 from openerp import tools
46 from openerp.tools.translate import _
47 from openerp.osv.osv import except_osv
48 from urllib import urlencode, quote as quote
50 _logger = logging.getLogger(__name__)
53 # We use a jinja2 sandboxed environment to render mako templates.
54 # Note that the rendering does not cover all the mako syntax, in particular
55 # arbitrary Python statements are not accepted, and not all expressions are
56 # allowed: only "public" attributes (not starting with '_') of objects may
58 # This is done on purpose: it prevents incidental or malicious execution of
59 # Python code that may break the security of the server.
60 from jinja2.sandbox import SandboxedEnvironment
61 mako_template_env = SandboxedEnvironment(
62 block_start_string="<%",
63 block_end_string="%>",
64 variable_start_string="${",
65 variable_end_string="}",
66 comment_start_string="<%doc>",
67 comment_end_string="</%doc>",
68 line_statement_prefix="%",
69 line_comment_prefix="##",
70 trim_blocks=True, # do not output newline after blocks
71 autoescape=True, # XML/HTML automatic escaping
73 mako_template_env.globals.update({
76 'urlencode': urlencode,
79 _logger.warning("jinja2 not available, templating features will not work!")
81 def mako_template(text):
82 """Build a Mako template.
84 This template uses UTF-8 encoding
87 return mako_template_env.from_string(text)
89 _extender_functions = {}
91 def webkit_report_extender(report_name):
93 A decorator to define functions to extend the context used in a template rendering.
94 report_name must be the xml id of the desired report (it is mandatory to indicate the
95 module in that xml id).
97 The given function will be called at the creation of the report. The following arguments
98 will be passed to it (in this order):
99 - pool The model pool.
102 - localcontext The context given to the template engine to render the templates for the
103 current report. This is the context that should be modified.
104 - context The OpenERP context.
107 lst = _extender_functions.get(report_name)
110 _extender_functions[report_name] = lst
116 class WebKitParser(report_sxw):
117 """Custom class that use webkit to render HTML reports
118 Code partially taken from report openoffice. Thanks guys :)
120 def __init__(self, name, table, rml=False, parser=rml_parse,
121 header=True, store=False, register=True):
122 self.parser_instance = False
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 tmp_dir = tempfile.gettempdir()
160 out_filename = tempfile.mktemp(suffix=".pdf", 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 head_file = file( os.path.join(
173 str(time.time()) + '.head.html'
177 head_file.write(self._sanitize_html(header.encode('utf-8')))
179 file_to_del.append(head_file.name)
180 command.extend(['--header-html', head_file.name])
182 foot_file = file( os.path.join(
184 str(time.time()) + '.foot.html'
188 foot_file.write(self._sanitize_html(footer.encode('utf-8')))
190 file_to_del.append(foot_file.name)
191 command.extend(['--footer-html', foot_file.name])
193 if webkit_header.margin_top :
194 command.extend(['--margin-top', str(webkit_header.margin_top).replace(',', '.')])
195 if webkit_header.margin_bottom :
196 command.extend(['--margin-bottom', str(webkit_header.margin_bottom).replace(',', '.')])
197 if webkit_header.margin_left :
198 command.extend(['--margin-left', str(webkit_header.margin_left).replace(',', '.')])
199 if webkit_header.margin_right :
200 command.extend(['--margin-right', str(webkit_header.margin_right).replace(',', '.')])
201 if webkit_header.orientation :
202 command.extend(['--orientation', str(webkit_header.orientation).replace(',', '.')])
203 if webkit_header.format :
204 command.extend(['--page-size', str(webkit_header.format).replace(',', '.')])
206 for html in html_list :
207 html_file = file(os.path.join(tmp_dir, str(time.time()) + str(count) +'.body.html'), 'w')
209 html_file.write(self._sanitize_html(html.encode('utf-8')))
211 file_to_del.append(html_file.name)
212 command.append(html_file.name)
213 command.append(out_filename)
214 stderr_fd, stderr_path = tempfile.mkstemp(text=True)
215 file_to_del.append(stderr_path)
217 status = subprocess.call(command, stderr=stderr_fd)
218 os.close(stderr_fd) # ensure flush before reading
219 stderr_fd = None # avoid closing again in finally block
220 fobj = open(stderr_path, 'r')
221 error_message = fobj.read()
223 if not error_message:
224 error_message = _('No diagnosis message was provided')
226 error_message = _('The following diagnosis message was provided:\n') + error_message
228 raise except_osv(_('Webkit error' ),
229 _("The command 'wkhtmltopdf' failed with error code = %s. Message: %s") % (status, error_message))
230 pdf_file = open(out_filename, 'rb')
231 pdf = pdf_file.read()
234 if stderr_fd is not None:
236 for f_to_del in file_to_del:
239 except (OSError, IOError), exc:
240 _logger.error('cannot remove file %s: %s', f_to_del, exc)
243 def translate_call(self, src):
244 """Translate String."""
245 ir_translation = self.pool['ir.translation']
246 name = self.tmpl and 'addons/' + self.tmpl or None
247 res = ir_translation._get_source(self.parser_instance.cr, self.parser_instance.uid,
248 name, 'report', self.parser_instance.localcontext.get('lang', 'en_US'), src)
250 # no translation defined, fallback on None (backward compatibility)
251 res = ir_translation._get_source(self.parser_instance.cr, self.parser_instance.uid,
252 None, 'report', self.parser_instance.localcontext.get('lang', 'en_US'), src)
257 # override needed to keep the attachments storing procedure
258 def create_single_pdf(self, cursor, uid, ids, data, report_xml, context=None):
259 """generate the PDF"""
261 # just try to find an xml id for the report
263 pool = openerp.registry(cr.dbname)
264 found_xml_ids = pool["ir.model.data"].search(cr, uid, [["model", "=", "ir.actions.report.xml"], \
265 ["res_id", "=", report_xml.id]], context=context)
268 xml_id = pool["ir.model.data"].read(cr, uid, found_xml_ids[0], ["module", "name"])
269 xml_id = "%s.%s" % (xml_id["module"], xml_id["name"])
274 if report_xml.report_type != 'webkit':
275 return super(WebKitParser,self).create_single_pdf(cursor, uid, ids, data, report_xml, context=context)
277 self.parser_instance = self.parser(cursor,
283 objs = self.getObjects(cursor, uid, ids, context)
284 self.parser_instance.set_context(objs, data, ids, report_xml.report_type)
288 if report_xml.report_file :
289 path = get_module_resource(*report_xml.report_file.split('/'))
290 if path and os.path.exists(path) :
291 template = file(path).read()
292 if not template and report_xml.report_webkit_data :
293 template = report_xml.report_webkit_data
295 raise except_osv(_('Error!'), _('Webkit report template not found!'))
296 header = report_xml.webkit_header.html
297 footer = report_xml.webkit_header.footer_html
298 if not header and report_xml.header:
300 _('No header defined for this Webkit report!'),
301 _('Please set a header in company settings.')
303 if not report_xml.header :
305 default_head = get_module_resource('report_webkit', 'default_header.html')
306 with open(default_head,'r') as f:
308 css = report_xml.webkit_header.css
312 body_mako_tpl = mako_template(template)
313 helper = WebKitHelper(cursor, uid, report_xml.id, context)
314 self.parser_instance.localcontext['helper'] = helper
315 self.parser_instance.localcontext['css'] = css
316 self.parser_instance.localcontext['_'] = self.translate_call
318 # apply extender functions
320 if xml_id in _extender_functions:
321 for fct in _extender_functions[xml_id]:
322 fct(pool, cr, uid, self.parser_instance.localcontext, context)
324 if report_xml.precise_mode:
325 ctx = dict(self.parser_instance.localcontext)
326 for obj in self.parser_instance.localcontext['objects']:
327 ctx['objects'] = [obj]
329 html = body_mako_tpl.render(dict(ctx))
334 raise except_osv(_('Webkit render!'), msg)
337 html = body_mako_tpl.render(dict(self.parser_instance.localcontext))
342 raise except_osv(_('Webkit render!'), msg)
343 head_mako_tpl = mako_template(header)
345 head = head_mako_tpl.render(dict(self.parser_instance.localcontext, _debug=False))
347 raise except_osv(_('Webkit render!'), u"%s" % e)
350 foot_mako_tpl = mako_template(footer)
352 foot = foot_mako_tpl.render(dict({},
353 **self.parser_instance.localcontext))
357 raise except_osv(_('Webkit render!'), msg)
358 if report_xml.webkit_debug :
360 deb = head_mako_tpl.render(dict(self.parser_instance.localcontext, _debug=tools.ustr("\n".join(htmls))))
364 raise except_osv(_('Webkit render!'), msg)
366 bin = self.get_lib(cursor, uid)
367 pdf = self.generate_pdf(bin, report_xml, head, foot, htmls)
370 def create(self, cursor, uid, ids, data, context=None):
371 """We override the create function in order to handle generator
372 Code taken from report openoffice. Thanks guys :) """
373 pool = openerp.registry(cursor.dbname)
374 ir_obj = pool['ir.actions.report.xml']
375 report_xml_ids = ir_obj.search(cursor, uid,
376 [('report_name', '=', self.name[7:])], context=context)
379 report_xml = ir_obj.browse(cursor,
383 report_xml.report_rml = None
384 report_xml.report_rml_content = None
385 report_xml.report_sxw_content_data = None
386 report_xml.report_sxw_content = None
387 report_xml.report_sxw = None
389 return super(WebKitParser, self).create(cursor, uid, ids, data, context)
390 if report_xml.report_type != 'webkit':
391 return super(WebKitParser, self).create(cursor, uid, ids, data, context)
392 result = self.create_source_pdf(cursor, uid, ids, data, report_xml, context)
397 def _sanitize_html(self, html):
398 """wkhtmltopdf expects the html page to declare a doctype.
400 if html and html[:9].upper() != "<!DOCTYPE":
401 html = "<!DOCTYPE html>\n" + html
404 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: