[IMP] Added the route converting all reports to pdf thanks to wkhtmltopdf. The html rendered report is downloaded via werkzeug.test.client and then parsed into an lxml.etree in order to extract only the useful data : local css, header, content and footer. We then generate a minimalist html page that is passed to wkhtmltopdf. Save in attachment feature is handled. A method transform a paperformat object into a list of parameters for wkhtmltopdf. Multiple IDS reports are generated in different pdf merged at the end.

bzr revid: openerp-sle@openerp-sle.home-20140212180934-dupp8x2ivo52uzib
This commit is contained in:
openerp-sle 2014-02-12 19:09:34 +01:00
parent 5fa1be1303
commit 8dbd892d8b
3 changed files with 362 additions and 0 deletions

View File

@ -19,10 +19,22 @@
#
##############################################################################
from openerp.osv.osv import except_osv
from openerp.addons.web import http
from openerp.tools.translate import _
from openerp.addons.web.http import request
import time
import base64
import logging
import tempfile
import lxml.html
import subprocess
from pyPdf import PdfFileWriter, PdfFileReader
from werkzeug.test import Client
from werkzeug.wrappers import BaseResponse
from werkzeug.datastructures import Headers
_logger = logging.getLogger(__name__)
@ -67,6 +79,310 @@ class Report(http.Controller):
return request.registry['report'].render(request.cr, request.uid, [], report.report_file,
docargs, context=request.context)
@http.route(['/report/pdf/<path:path>'], type='http', auth="user", website=True)
def report_pdf(self, path=None, landscape=False, **post):
cr, uid, context = request.cr, request.uid, request.context
# Get the report we are working on.
# Pattern is /report/module.reportname(?a=1)
reportname_in_path = path.split('/')[1].split('?')[0]
report = self._get_report_from_name(reportname_in_path)
# Check attachment_use field. If set to true and an existing pdf is already saved, return
# this one now. If not, mark save it.
save_in_attachment = {}
if report.attachment_use is True:
# Get the record ids we are working on.
path_ids = [int(i) for i in path.split('/')[2].split('?')[0].split(',')]
save_in_attachment['model'] = report.model
save_in_attachment['loaded_documents'] = {}
for path_id in path_ids:
obj = request.registry[report.model].browse(cr, uid, path_id)
filename = eval(report.attachment, {'object': obj, 'time': time})
if filename is False: # May be false if, for instance, the record is in draft state
continue
else:
alreadyindb = [('datas_fname', '=', filename),
('res_model', '=', report.model),
('res_id', '=', path_id)]
attach_ids = request.registry['ir.attachment'].search(cr, uid, alreadyindb)
if attach_ids:
# Add the loaded pdf in the loaded_documents list
pdf = request.registry['ir.attachment'].browse(cr, uid, attach_ids[0]).datas
pdf = base64.decodestring(pdf)
save_in_attachment['loaded_documents'][path_id] = pdf
_logger.info('The PDF document %s was loaded from the database' % filename)
else:
# Mark current document to be saved
save_in_attachment[path_id] = filename
# Get the paperformat associated to the report, if there is.
if not report.paperformat_id:
user = request.registry['res.users'].browse(cr, uid, uid, context=context)
paperformat = user.company_id.paperformat_id
else:
paperformat = report.paperformat_id
# Get the html report.
html = self._get_url_content('/' + path, post)
# Get some css and script in order to build a minimal html page for the report.
# This page will later be sent to wkhtmltopdf.
css = self._get_url_content('/report/static/src/css/reset.min.css')
css += self._get_url_content('/website/static/src/css/website.css')
subst = self._get_url_content('/report/static/src/js/subst.js')
headerhtml = []
contenthtml = []
footerhtml = []
minimalhtml = """
<html style="height: 0mm;">
<head>
<style type='text/css'>{0}</style>
<link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.1.0/css/bootstrap.min.css">
<script type='text/javascript'>{1}</script>
</head>
<body class="container" onload='subst()'>
{2}
</body>
</html>"""
# The retrieved html report must be simplified. We convert it into a xml tree
# via lxml in order to extract header, footer and all reportcontent.
try:
root = lxml.html.fromstring(html)
for node in root.xpath("//html/head/style"):
css += node.text
for node in root.xpath("//div[@class='header']"):
body = lxml.html.tostring(node)
header = minimalhtml.format(css, subst, body)
headerhtml.append(header)
for node in root.xpath("//div[@class='footer']"):
body = lxml.html.tostring(node)
footer = minimalhtml.format(css, subst, body)
footerhtml.append(footer)
for node in root.xpath("//div[@class='page']"):
# Previously, we marked some reports to be saved in attachment via their ids, so we
# must set a relation between report ids and reportcontent. We use the QWeb
# branding in order to do so: searching after a node having a data-oe-model
# attribute with the value of the current report model and read its oe-id attribute
oemodelnode = node.find(".//*[@data-oe-model='" + report.model + "']")
if oemodelnode is not None:
reportid = oemodelnode.get('data-oe-id', False)
if reportid is not False:
reportid = int(reportid)
else:
reportid = False
body = lxml.html.tostring(node)
reportcontent = minimalhtml.format(css, '', body)
contenthtml.append(tuple([reportid, reportcontent]))
except lxml.etree.XMLSyntaxError:
contenthtml = []
contenthtml.append(html)
save_in_attachment = {} # Don't save this potentially malformed document
# Get paperformat arguments set in the root html tag. They are prioritized over
# paperformat-record arguments.
specific_paperformat_args = {}
for attribute in root.items():
if attribute[0].startswith('data-report-'):
specific_paperformat_args[attribute[0]] = attribute[1]
# Execute wkhtmltopdf process.
pdf = self._generate_wkhtml_pdf(headerhtml, footerhtml, contenthtml, landscape,
paperformat, specific_paperformat_args, save_in_attachment)
return self._make_pdf_response(pdf)
def _get_url_content(self, url, post=None):
"""Resolve an internal webpage url and return its content with the help of
werkzeug.test.client.
:param url: string representinf the url to resolve
:param post: a dict representing the query string
:returns: a tuple str(html), int(statuscode)
"""
# Rebuilding the query string.
if post:
url += '?'
url += '&'.join('%s=%s' % (k, v) for (k, v) in post.iteritems())
# We have to pass the current headers in order to see the report.
reqheaders = Headers(request.httprequest.headers)
response = Client(request.httprequest.app, BaseResponse).get(url, headers=reqheaders,
follow_redirects=True)
content = response.data
try:
content = content.decode('utf-8')
except UnicodeDecodeError:
pass
return content
def _generate_wkhtml_pdf(self, headers, footers, bodies, landscape,
paperformat, spec_paperformat_args=None, save_in_attachment=None):
"""Execute wkhtmltopdf as a subprocess in order to convert html given in input into a pdf
document.
:param header: list of string containing the headers
:param footer: list of string containing the footers
:param bodies: list of string containing the reports
:param landscape: boolean to force the pdf to be rendered under a landscape format
:param paperformat: ir.actions.report.paperformat to generate the wkhtmltopf arguments
:param specific_paperformat_args: dict of prioritized paperformat arguments
:param save_in_attachment: dict of reports to save/load in/from the db
:returns: Content of the pdf as a string
"""
command = ['wkhtmltopdf-0.12']
tmp_dir = tempfile.gettempdir()
# Display arguments
command_args = []
if paperformat:
command_args.extend(self._build_wkhtmltopdf_args(paperformat, spec_paperformat_args))
if landscape and '--orientation' in command_args:
command_args_copy = list(command_args)
for index, elem in enumerate(command_args_copy):
if elem == '--orientation':
del command_args[index]
del command_args[index]
command_args.extend(['--orientation', 'landscape'])
elif landscape and not '--orientation' in command_args:
command_args.extend(['--orientation', 'landscape'])
pdfdocuments = []
# HTML to PDF thanks to WKhtmltopdf
for index, reporthtml in enumerate(bodies):
command_arg_local = []
pdfreport = tempfile.NamedTemporaryFile(suffix='.pdf', prefix='report.tmp.',
mode='w+b')
# Directly load the document if we have it
if save_in_attachment and save_in_attachment['loaded_documents'].get(reporthtml[0]):
pdfreport.write(save_in_attachment['loaded_documents'].get(reporthtml[0]))
pdfreport.seek(0)
pdfdocuments.append(pdfreport)
continue
# Header stuff
if headers:
head_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.header.tmp.',
dir=tmp_dir, mode='w+')
head_file.write(headers[index])
head_file.seek(0)
command_arg_local.extend(['--header-html', head_file.name])
# Footer stuff
if footers:
foot_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.footer.tmp.',
dir=tmp_dir, mode='w+')
foot_file.write(footers[index])
foot_file.seek(0)
command_arg_local.extend(['--footer-html', foot_file.name])
# Body stuff
content_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.body.tmp.',
dir=tmp_dir, mode='w+')
content_file.write(reporthtml[1])
content_file.seek(0)
try:
wkhtmltopdf = command + command_args + command_arg_local
wkhtmltopdf += [content_file.name] + [pdfreport.name]
process = subprocess.Popen(wkhtmltopdf, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
out, err = process.communicate()
if process.returncode != 0:
raise except_osv(_('Report (PDF)'),
_('wkhtmltopdf-patched failed with error code = %s. '
'Message: %s') % (str(process.returncode), err))
# Save the pdf in attachment if marked
if reporthtml[0] is not False and save_in_attachment.get(reporthtml[0]):
attachment = {
'name': save_in_attachment.get(reporthtml[0]),
'datas': base64.encodestring(pdfreport.read()),
'datas_fname': save_in_attachment.get(reporthtml[0]),
'res_model': save_in_attachment.get('model'),
'res_id': reporthtml[0],
}
request.registry['ir.attachment'].create(request.cr, request.uid, attachment)
_logger.info('The PDF document %s is now saved in the '
'database' % attachment['name'])
pdfreport.seek(0)
pdfdocuments.append(pdfreport)
if headers:
head_file.close()
if footers:
foot_file.close()
except:
raise
# Get and return the full pdf
if len(pdfdocuments) == 1:
content = pdfdocuments[0].read()
pdfdocuments[0].close()
else:
content = self._merge_pdf(pdfdocuments)
return content
def _build_wkhtmltopdf_args(self, paperformat, specific_paperformat_args=None):
"""Build arguments understandable by wkhtmltopdf from an ir.actions.report.paperformat
record.
:paperformat: ir.actions.report.paperformat record associated to a document
:specific_paperformat_args: a dict containing prioritized wkhtmltopdf arguments
:returns: list of string containing the wkhtmltopdf arguments
"""
command_args = []
if paperformat.format and paperformat.format != 'custom':
command_args.extend(['--page-size', paperformat.format])
if paperformat.page_height and paperformat.page_width and paperformat.format == 'custom':
command_args.extend(['--page-width', str(paperformat.page_width) + 'in'])
command_args.extend(['--page-height', str(paperformat.page_height) + 'in'])
if specific_paperformat_args and specific_paperformat_args['data-report-margin-top']:
command_args.extend(['--margin-top',
str(specific_paperformat_args['data-report-margin-top'])])
else:
command_args.extend(['--margin-top', str(paperformat.margin_top)])
if paperformat.margin_left:
command_args.extend(['--margin-left', str(paperformat.margin_left)])
if paperformat.margin_bottom:
command_args.extend(['--margin-bottom', str(paperformat.margin_bottom)])
if paperformat.margin_right:
command_args.extend(['--margin-right', str(paperformat.margin_right)])
if paperformat.orientation:
command_args.extend(['--orientation', str(paperformat.orientation)])
if paperformat.header_spacing:
command_args.extend(['--header-spacing', str(paperformat.header_spacing)])
if paperformat.header_line:
command_args.extend(['--header-line'])
if paperformat.dpi:
command_args.extend(['--dpi', str(paperformat.dpi)])
return command_args
def _get_report_from_name(self, report_name):
"""Get the first record of ir.actions.report.xml having the argument as value for
the field report_name.
@ -81,3 +397,33 @@ class Report(http.Controller):
report = report_obj.browse(request.cr, request.uid, idreport[0],
context=request.context)
return report
def _make_pdf_response(self, pdf):
"""Make a request response for a PDF file with correct http headers.
:param pdf: content of a pdf in a string
:returns: request response for a pdf document
"""
pdfhttpheaders = [('Content-Type', 'application/pdf'),
('Content-Length', len(pdf))]
return request.make_response(pdf, headers=pdfhttpheaders)
def _merge_pdf(self, documents):
"""Merge PDF files into one.
:param documents: list of pdf files
:returns: string containing the merged pdf
"""
writer = PdfFileWriter()
for document in documents:
reader = PdfFileReader(file(document.name, "rb"))
for page in range(0, reader.getNumPages()):
writer.addPage(reader.getPage(page))
document.close()
merged = tempfile.NamedTemporaryFile(suffix='.pdf', prefix='report.tmp.', mode='w+b')
writer.write(merged)
merged.seek(0)
content = merged.read()
merged.close()
return content

View File

@ -0,0 +1,2 @@
/* reset5 2011 opensource.736cs.com MIT - https://code.google.com/p/reset5 */
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,audio,canvas,details,figcaption,figure,footer,header,hgroup,mark,menu,meter,nav,output,progress,section,summary,time,video{border:0;outline:0;font-size:100%;vertical-align:baseline;background:transparent;margin:0;padding:0;}body{line-height:1;}article,aside,dialog,figure,footer,header,hgroup,nav,section,blockquote{display:block;}nav ul{list-style:none;}ol{list-style:decimal;}ul{list-style:disc;}ul ul{list-style:circle;}blockquote,q{quotes:none;}blockquote:before,blockquote:after,q:before,q:after{content:none;}ins{text-decoration:underline;}del{text-decoration:line-through;}mark{background:none;}abbr[title],dfn[title]{border-bottom:1px dotted #000;cursor:help;}table{border-collapse:collapse;border-spacing:0;}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0;}input[type=submit],input[type=button],button{margin:0!important;padding:0!important;}input,select,a img{vertical-align:middle;}

View File

@ -0,0 +1,14 @@
function subst() {
var vars = {};
var x = document.location.search.substring(1).split('&');
for (var i in x) {
var z = x[i].split('=', 2);
vars[z[0]] = unescape(z[1]);
}
var x=['frompage', 'topage', 'page', 'webpage', 'section', 'subsection', 'subsubsection'];
for (var i in x) {
var y = document.getElementsByClassName(x[i]);
for (var j=0; j<y.length; ++j)
y[j].textContent = vars[x[i]];
}
}