[MERGE][REF] Report module: Moved methods generating pdf/html from Controller to Model in order to be able to print without request (report controllers still exist but are now just interfaces to these methods) ; adapted account_vat report this way

bzr revid: sle@openerp.com-20140324181616-5kty54mh9zg2mlj3
This commit is contained in:
Simon Lejeune 2014-03-24 19:16:16 +01:00
commit c2e7a69eba
14 changed files with 626 additions and 710 deletions

View File

@ -19,6 +19,7 @@
#
##############################################################################
from openerp.osv import osv
from openerp.addons.web import http
from openerp.addons.web.http import request
from common_report_header import common_report_header
@ -29,14 +30,12 @@ except ImportError:
import xlwt
class tax_report(http.Controller, common_report_header):
class tax_report(osv.AbstractModel, common_report_header):
_name = 'report.account.report_vat'
@http.route(['/report/account.report_vat'], type='http', auth='user', website=True, multilang=True)
def report_account_tax(self, **data):
def render_html(self, cr, uid, ids, data=None, context=None):
report_obj = request.registry['report']
self.cr, self.uid, self.pool = request.cr, request.uid, request.registry
data = report_obj.eval_params(data)
self.cr, self.uid, self.context = cr, uid, context
res = {}
self.period_ids = []
@ -54,23 +53,23 @@ class tax_report(http.Controller, common_report_header):
'based_on': self._get_basedon(data),
'period_from': self.get_start_period(data),
'period_to': self.get_end_period(data),
'taxlines': self._get_lines(self._get_basedon(data), company_id=data['form']['company_id']),
'taxlines': self._get_lines(self._get_basedon(data), company_id=data['form']['company_id'], cr=cr, uid=uid),
}
return request.registry['report'].render(self.cr, self.uid, [], 'account.report_vat', docargs)
return report_obj.render(self.cr, self.uid, [], 'account.report_vat', docargs, context=context)
def _get_basedon(self, form):
return form['form']['based_on']
def _get_lines(self, based_on, company_id=False, parent=False, level=0, context=None):
def _get_lines(self, based_on, company_id=False, parent=False, level=0, context=None, cr=None, uid=None):
period_list = self.period_ids
res = self._get_codes(based_on, company_id, parent, level, period_list, context=context)
res = self._get_codes(based_on, company_id, parent, level, period_list, cr=cr, uid=uid, context=context)
if period_list:
res = self._add_codes(based_on, res, period_list, context=context)
else:
self.cr.execute ("select id from account_fiscalyear")
fy = self.cr.fetchall()
self.cr.execute ("select id from account_period where fiscalyear_id = %s",(fy[0][0],))
periods = self.cr.fetchall()
cr.execute ("select id from account_fiscalyear")
fy = cr.fetchall()
cr.execute ("select id from account_period where fiscalyear_id = %s",(fy[0][0],))
periods = cr.fetchall()
for p in periods:
period_list.append(p[0])
res = self._add_codes(based_on, res, period_list, context=context)
@ -90,7 +89,7 @@ class tax_report(http.Controller, common_report_header):
}
top_result.append(res_dict)
res_general = self._get_general(res[i][1].id, period_list, company_id, based_on, context=context)
res_general = self._get_general(res[i][1].id, period_list, company_id, based_on, cr=cr, uid=uid, context=context)
ind_general = 0
while ind_general < len(res_general):
res_general[ind_general]['type'] = 2
@ -101,14 +100,14 @@ class tax_report(http.Controller, common_report_header):
i+=1
return top_result
def _get_general(self, tax_code_id, period_list, company_id, based_on, context=None):
def _get_general(self, tax_code_id, period_list, company_id, based_on, cr=None, uid=None, context=None):
if not self.display_detail:
return []
res = []
obj_account = self.pool.get('account.account')
periods_ids = tuple(period_list)
if based_on == 'payments':
self.cr.execute('SELECT SUM(line.tax_amount) AS tax_amount, \
cr.execute('SELECT SUM(line.tax_amount) AS tax_amount, \
SUM(line.debit) AS debit, \
SUM(line.credit) AS credit, \
COUNT(*) AS count, \
@ -132,7 +131,7 @@ class tax_report(http.Controller, common_report_header):
company_id, periods_ids, 'paid',))
else:
self.cr.execute('SELECT SUM(line.tax_amount) AS tax_amount, \
cr.execute('SELECT SUM(line.tax_amount) AS tax_amount, \
SUM(line.debit) AS debit, \
SUM(line.credit) AS credit, \
COUNT(*) AS count, \
@ -149,23 +148,21 @@ class tax_report(http.Controller, common_report_header):
AND account.active \
GROUP BY account.id,account.name,account.code', ('draft', tax_code_id,
company_id, periods_ids,))
res = self.cr.dictfetchall()
res = cr.dictfetchall()
i = 0
while i<len(res):
res[i]['account'] = obj_account.browse(self.cr, self.uid, res[i]['account_id'], context=context)
res[i]['account'] = obj_account.browse(cr, uid, res[i]['account_id'], context=context)
i+=1
return res
def _get_codes(self, based_on, company_id, parent=False, level=0, period_list=None, context=None):
def _get_codes(self, based_on, company_id, parent=False, level=0, period_list=None, cr=None, uid=None, context=None):
obj_tc = self.pool.get('account.tax.code')
ids = obj_tc.search(self.cr, self.uid, [('parent_id','=',parent),('company_id','=',company_id)], order='sequence', context=context)
ids = obj_tc.search(cr, uid, [('parent_id', '=', parent), ('company_id', '=', company_id)], order='sequence', context=context)
res = []
for code in obj_tc.browse(self.cr, self.uid, ids, {'based_on': based_on}):
for code in obj_tc.browse(cr, uid, ids, {'based_on': based_on}):
res.append(('.'*2*level, code))
res += self._get_codes(based_on, company_id, code.id, level+1, context=context)
res += self._get_codes(based_on, company_id, code.id, level+1, cr=cr, uid=uid, context=context)
return res
def _add_codes(self, based_on, account_list=None, period_list=None, context=None):
@ -187,9 +184,6 @@ class tax_report(http.Controller, common_report_header):
res.append((account[0], code))
return res
def _get_currency(self, form, context=None):
return self.pool.get('res.company').browse(self.cr, self.uid, form['company_id'], context=context).currency_id.name
def sort_result(self, accounts, context=None):
result_accounts = []
ind=0
@ -206,7 +200,8 @@ class tax_report(http.Controller, common_report_header):
bcl_rup_ind = ind - 1
while (bcl_current_level >= int(accounts[bcl_rup_ind]['level']) and bcl_rup_ind >= 0 ):
res_tot = { 'code': accounts[bcl_rup_ind]['code'],
res_tot = {
'code': accounts[bcl_rup_ind]['code'],
'name': '',
'debit': 0,
'credit': 0,
@ -229,25 +224,23 @@ class tax_report(http.Controller, common_report_header):
return result_accounts
class tax_report_xls(http.Controller):
@http.route(['/report/account.report_vat_xls'], type='http', auth='user', website=True, multilang=True)
def report_account_tax_xls(self, **data):
report_obj = request.registry['report']
self.cr, self.uid, self.pool = request.cr, request.uid, request.registry
data = report_obj.eval_params(data)
# Very ugly lines, only for the proof of concept of 'controller' report
taxreport_obj = request.registry['report.account.report_vat']
from openerp.addons.report.controllers.main import ReportController
eval_params = ReportController()._eval_params
res = {}
self.period_ids = []
period_obj = self.pool.get('account.period')
self.display_detail = data['form']['display_detail']
res['periods'] = ''
res['fiscalyear'] = data['form'].get('fiscalyear_id', False)
cr, uid = request.cr, request.uid
data = eval_params(data)
data = {'form': data}
if data['form'].get('period_from', False) and data['form'].get('period_to', False):
self.period_ids = period_obj.build_ctx_periods(self.cr, self.uid, data['form']['period_from'], data['form']['period_to'])
content = ''
lines = self._get_lines(self._get_basedon(data), company_id=data['form']['company_id'])
taxreport_obj.render_html(cr, uid, [], data=data)
lines = taxreport_obj._get_lines(taxreport_obj._get_basedon(data), company_id=data['form']['company_id'], cr=cr, uid=uid)
if lines:
xls = StringIO.StringIO()

View File

@ -449,7 +449,7 @@ class email_template(osv.osv):
ctx['lang'] = self.render_template_batch(cr, uid, template.lang, template.model, [res_id], context)[res_id] # take 0 ?
if report.report_type in ['qweb-html', 'qweb-pdf']:
result, format = self.pool['report'].get_pdf(report, res_id, context=ctx), 'pdf'
result, format = self.pool['report'].get_pdf(cr, uid, [res_id], report_service, context=ctx), 'pdf'
else:
result, format = openerp.report.render_report(cr, uid, [res_id], report_service, {'model': template.model}, ctx)

View File

@ -19,24 +19,22 @@
#
##############################################################################
from openerp.addons.web import http
from openerp.addons.web.http import request
from openerp.osv import osv
class bom_structure(http.Controller):
class bom_structure(osv.AbstractModel):
_name = 'report.mrp.report_mrpbomstructure'
@http.route(['/report/mrp.report_mrpbomstructure/<docids>'], type='http', auth='user', website=True, multilang=True)
def report_mrpbomstructure(self, docids):
ids = [int(i) for i in docids.split(',')]
ids = list(set(ids))
report_obj = request.registry['mrp.bom']
docs = report_obj.browse(request.cr, request.uid, ids, context=request.context)
def render_html(self, cr, uid, ids, data=None, context=None):
mrpbom_obj = self.pool['mrp.bom']
report_obj = self.pool['report']
docs = mrpbom_obj.browse(cr, uid, ids, context=context)
docargs = {
'docs': docs,
'get_children': self.get_children,
}
return request.registry['report'].render(request.cr, request.uid, [], 'mrp.report_mrpbomstructure', docargs)
return report_obj.render(cr, uid, [], 'mrp.report_mrpbomstructure', docargs, context=context)
def get_children(self, object, level=0):
result = []

View File

@ -446,12 +446,7 @@ class purchase_order(osv.osv):
'''
assert len(ids) == 1, 'This option should only be used for a single id at a time'
self.signal_send_rfq(cr, uid, ids)
datas = {
'model': 'purchase.order',
'ids': ids,
'form': self.read(cr, uid, ids[0], context=context),
}
return {'type': 'ir.actions.report.xml', 'report_name': 'purchase.quotation', 'datas': datas, 'nodestroy': True}
return self.pool['report'].get_action(cr, uid, ids, 'purchase.report_purchasequotation', context=context)
#TODO: implement messages system
def wkf_confirm_order(self, cr, uid, ids, context=None):

View File

@ -19,459 +19,62 @@
#
##############################################################################
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 openerp.tools.config as config
from openerp.addons.web.http import Controller, route, request
import time
import base64
import logging
import tempfile
import lxml.html
import subprocess
import simplejson
try:
import cStringIO as StringIO
except ImportError:
import StringIO
import psutil
import signal
import os
from distutils.version import LooseVersion
import urlparse
from werkzeug import exceptions
from werkzeug.test import Client
from werkzeug.wrappers import BaseResponse
from werkzeug.datastructures import Headers
from reportlab.graphics.barcode import createBarcodeDrawing
_logger = logging.getLogger(__name__)
try:
from pyPdf import PdfFileWriter, PdfFileReader
except ImportError:
PdfFileWriter = PdfFileReader = None
class ReportController(Controller):
class Report(http.Controller):
@http.route(['/report/<reportname>/<docids>'], type='http', auth='user', website=True, multilang=True)
def report_html(self, reportname, docids, **kwargs):
"""This is the generic route for QWeb reports. It is used for reports
which do not need to preprocess the data (i.e. reports that just display
fields of a record).
It is given a ~fully qualified report name, for instance 'account.report_invoice'.
Based on it, we know the module concerned and the name of the template. With the
name of the template, we will make a search on the ir.actions.reports.xml table and
get the record associated to finally know the model this template refers to.
There is a way to declare the report (in module_report(s).xml) that you must respect:
id="action_report_model"
model="module.model" # To know which model the report refers to
string="Invoices"
report_type="qweb-pdf" # or qweb-html
name="module.template_name"
file="module.template_name"
If you don't want your report listed under the print button, just add
'menu=False'.
"""
ids = [int(i) for i in docids.split(',')]
ids = list(set(ids))
report = self._get_report_from_name(reportname)
report_obj = request.registry[report.model]
docs = report_obj.browse(request.cr, request.uid, ids, context=request.context)
docargs = {
'doc_ids': ids,
'doc_model': report.model,
'docs': docs,
}
return request.registry['report'].render(request.cr, request.uid, [], report.report_name,
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):
"""Route converting any reports to pdf. It will get the html-rendered report, extract
header, page and footer in order to prepare minimal html pages that will be further passed
to wkhtmltopdf.
:param path: URL of the report (e.g. /report/account.report_invoice/1)
:returns: a response with 'application/pdf' headers and the pdf as content
"""
#------------------------------------------------------
# Generic reports controller
#------------------------------------------------------
@route('/report/<reportname>/<docids>', type='http', auth='user', website=True, multilang=True)
def report_html(self, reportname, docids):
cr, uid, context = request.cr, request.uid, request.context
docids = self._eval_params(docids)
return request.registry['report'].get_html(cr, uid, docids, reportname, context=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, load
# 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 not, get the one associated to
# the company.
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)[0]
subst = self._get_url_content('/report/static/src/js/subst.js')[0] # Used in age numbering
css = '' # Local css
headerhtml = []
contenthtml = []
footerhtml = []
base_url = request.registry['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
minimalhtml = """
<base href="{3}">
<!DOCTYPE html>
<html style="height: 0;">
<head>
<link href="/report/static/src/css/reset.min.css" rel="stylesheet"/>
<link href="/web/static/lib/bootstrap/css/bootstrap.css" rel="stylesheet"/>
<link href="/website/static/src/css/website.css" rel="stylesheet"/>
<link href="/web/static/lib/fontawesome/css/font-awesome.css" rel="stylesheet"/>
<style type='text/css'>{0}</style>
<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 headers, footers and content.
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, base_url)
headerhtml.append(header)
for node in root.xpath("//div[@class='footer']"):
body = lxml.html.tostring(node)
footer = minimalhtml.format(css, subst, body, base_url)
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 report's content. 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, base_url)
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 representing 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 tuple([content, response.headers])
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']
tmp_dir = tempfile.gettempdir()
command_args = []
# Passing the cookie in order to resolve URL.
command_args.extend(['--cookie', 'session_id', request.httprequest.cookies['session_id']])
# Display arguments
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:
# If the server is running with only one worker, increase it to two to be able
# to serve the http request from wkhtmltopdf.
if config['workers'] == 1:
ppid = psutil.Process(os.getpid()).ppid
os.kill(ppid, signal.SIGTTIN)
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 config['workers'] == 1:
os.kill(ppid, signal.SIGTTOU)
if process.returncode != 0:
raise except_osv(_('Report (PDF)'),
_('wkhtmltopdf 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'])])
elif paperformat.margin_top:
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.
"""
report_obj = request.registry['ir.actions.report.xml']
qwebtypes = ['qweb-pdf', 'qweb-html']
idreport = report_obj.search(request.cr, request.uid,
[('report_type', 'in', qwebtypes),
('report_name', '=', report_name)])
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))]
@route('/report/pdf/report/<reportname>/<docids>', type='http', auth="user", website=True)
def report_pdf(self, reportname, docids):
cr, uid, context = request.cr, request.uid, request.context
docids = self._eval_params(docids)
pdf = request.registry['report'].get_pdf(cr, uid, docids, reportname, context=context)
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.
#------------------------------------------------------
# Particular reports controller
#------------------------------------------------------
@route('/report/<reportname>', type='http', auth='user', website=True, multilang=True)
def report_html_particular(self, reportname, **data):
cr, uid, context = request.cr, request.uid, request.context
report_obj = request.registry['report']
data = self._eval_params(data) # Sanitizing
return report_obj.get_html(cr, uid, [], reportname, data=data, context=context)
: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 = StringIO.StringIO()
writer.write(merged)
merged.seek(0)
content = merged.read()
merged.close()
return content
@route('/report/pdf/report/<reportname>', type='http', auth='user', website=True, multilang=True)
def report_pdf_particular(self, reportname, **data):
cr, uid, context = request.cr, request.uid, request.context
report_obj = request.registry['report']
data = self._eval_params(data) # Sanitizing
pdf = report_obj.get_pdf(cr, uid, [], reportname, data=data, context=context)
pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))]
return request.make_response(pdf, headers=pdfhttpheaders)
@http.route(['/report/barcode', '/report/barcode/<type>/<path:value>'], type='http', auth="user")
def barcode(self, type, value, width=300, height=50):
#------------------------------------------------------
# Misc. route utils
#------------------------------------------------------
@route(['/report/barcode', '/report/barcode/<type>/<path:value>'], type='http', auth="user")
def report_barcode(self, type, value, width=300, height=50):
"""Contoller able to render barcode images thanks to reportlab.
Samples:
<img t-att-src="'/report/barcode/QR/%s' % o.name"/>
<img t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('QR', o.name, 200, 200)"/>
<img t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' %
('QR', o.name, 200, 200)"/>
:param type: Accepted types: 'Codabar', 'Code11', 'Code128', 'EAN13', 'EAN8', 'Extended39',
'Extended93', 'FIM', 'I2of5', 'MSI', 'POSTNET', 'QR', 'Standard39', 'Standard93',
@ -488,52 +91,69 @@ class Report(http.Controller):
return request.make_response(barcode, headers=[('Content-Type', 'image/png')])
@http.route('/report/download', type='http', auth="user")
def report_attachment(self, data, token):
@route(['/report/download'], type='http', auth="user", website=True)
def report_download(self, data, token):
"""This function is used by 'qwebactionmanager.js' in order to trigger the download of
a report of any type.
a pdf report.
:param data: a javasscript array JSON.stringified containg report internal url ([0]) and
:param data: a javascript array JSON.stringified containg report internal url ([0]) and
type [1]
:returns: Response with a filetoken cookie and an attachment header
"""
requestcontent = simplejson.loads(data)
url, type = requestcontent[0], requestcontent[1]
file, fileheaders = self._get_url_content(url)
if type == 'qweb-pdf':
response = self._make_pdf_response(file)
response.headers.add('Content-Disposition', 'attachment; filename=report.pdf;')
elif type == 'controller':
response = request.make_response(file)
response.headers.add('Content-Disposition', fileheaders['Content-Disposition'])
response.headers.add('Content-Type', fileheaders['Content-Type'])
reportname = url.split('/report/pdf/report/')[1].split('?')[0].split('/')[0]
if '?' not in url:
# Generic report:
docids = url.split('/')[-1]
response = self.report_pdf(reportname, docids)
else:
# Particular report:
querystring = url.split('?')[1]
querystring = dict(urlparse.parse_qsl(querystring))
response = self.report_pdf_particular(reportname, **querystring)
response.headers.add('Content-Disposition', 'attachment; filename=%s.pdf;' % reportname)
response.set_cookie('fileToken', token)
return response
elif type =='controller':
from werkzeug.test import Client
from werkzeug.wrappers import BaseResponse
from werkzeug.datastructures import Headers
reqheaders = Headers(request.httprequest.headers)
response = Client(request.httprequest.app, BaseResponse).get(url, headers=reqheaders, follow_redirects=True)
response.set_cookie('fileToken', token)
return response
else:
return
response.headers.add('Content-Length', len(file))
response.set_cookie('fileToken', token)
return response
@http.route('/report/check_wkhtmltopdf', type='json', auth="user")
@route(['/report/check_wkhtmltopdf'], type='json', auth="user")
def check_wkhtmltopdf(self):
"""Check the presence of wkhtmltopdf and return its version. If wkhtmltopdf
cannot be found, return False.
return request.registry['report']._check_wkhtmltopdf()
def _eval_params(self, param):
"""Parse a dict generated by the webclient (javascript) into a python dict.
"""
try:
process = subprocess.Popen(['wkhtmltopdf', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = process.communicate()
if err:
raise
version = out.splitlines()[1].strip()
version = version.split(' ')[1]
if LooseVersion(version) < LooseVersion('0.12.0'):
_logger.warning('Upgrade WKHTMLTOPDF to (at least) 0.12.0')
return 'upgrade'
return True
except:
_logger.error('You need WKHTMLTOPDF to print a pdf version of this report.')
return False
if isinstance(param, dict):
for key, value in param.iteritems():
if value.lower() == 'false':
param[key] = False
elif value.lower() == 'true':
param[key] = True
elif ',' in value:
param[key] = [int(i) for i in value.split(',')]
else:
try:
param[key] = int(value)
except (ValueError, TypeError):
pass
else:
if isinstance(param, (str, unicode)):
param = [int(i) for i in param.split(',')]
if isinstance(param, list):
param = list(set(param))
if isinstance(param, int):
param = [param]
return param

View File

@ -19,54 +19,85 @@
#
##############################################################################
from openerp.addons.web.http import request
from openerp.osv import osv
from openerp.osv.fields import float as float_field, function as function_field, datetime as datetime_field
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
from openerp.tools.translate import _
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, config
from openerp.osv.fields import float as float_field, function as function_field, datetime as datetime_field
import os
import time
import psutil
import signal
import base64
import logging
import tempfile
import lxml.html
import cStringIO
import subprocess
from datetime import datetime
from werkzeug.datastructures import Headers
from werkzeug.wrappers import BaseResponse
from werkzeug.test import Client
from functools import partial
from distutils.version import LooseVersion
try:
from pyPdf import PdfFileWriter, PdfFileReader
except ImportError:
PdfFileWriter = PdfFileReader = None
def get_date_length(date_format=DEFAULT_SERVER_DATE_FORMAT):
return len((datetime.now()).strftime(date_format))
_logger = logging.getLogger(__name__)
class report(osv.Model):
"""Check the presence of wkhtmltopdf and return its version."""
wkhtmltopdf_state = 'install'
try:
process = subprocess.Popen(
['wkhtmltopdf', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
except OSError:
_logger.error('You need wkhtmltopdf to print a pdf version of the reports.')
else:
out, err = process.communicate()
version = out.splitlines()[1].strip()
version = version.split(' ')[1]
if LooseVersion(version) < LooseVersion('0.12.0'):
_logger.warning('Upgrade wkhtmltopdf to (at least) 0.12.0')
wkhtmltopdf_state = 'upgrade'
wkhtmltopdf_state = 'ok'
class Report(osv.Model):
_name = "report"
_description = "Report"
public_user = None
def get_digits(self, obj=None, f=None, dp=None):
#--------------------------------------------------------------------------
# Extension of ir_ui_view.render with arguments frequently used in reports
#--------------------------------------------------------------------------
def _get_digits(self, cr, uid, obj=None, f=None, dp=None):
d = DEFAULT_DIGITS = 2
if dp:
decimal_precision_obj = self.pool['decimal.precision']
ids = decimal_precision_obj.search(request.cr, request.uid, [('name', '=', dp)])
ids = decimal_precision_obj.search(cr, uid, [('name', '=', dp)])
if ids:
d = decimal_precision_obj.browse(request.cr, request.uid, ids)[0].digits
d = decimal_precision_obj.browse(cr, uid, ids)[0].digits
elif obj and f:
res_digits = getattr(obj._columns[f], 'digits', lambda x: ((16, DEFAULT_DIGITS)))
if isinstance(res_digits, tuple):
d = res_digits[1]
else:
d = res_digits(request.cr)[1]
d = res_digits(cr)[1]
elif (hasattr(obj, '_field') and
isinstance(obj._field, (float_field, function_field)) and
obj._field.digits):
d = obj._field.digits[1] or DEFAULT_DIGITS
return d
def _get_lang_dict(self):
def _get_lang_dict(self, cr, uid):
pool_lang = self.pool['res.lang']
lang = self.localcontext.get('lang', 'en_US') or 'en_US'
lang_ids = pool_lang.search(request.cr, request.uid, [('code', '=', lang)])[0]
lang_obj = pool_lang.browse(request.cr, request.uid, lang_ids)
lang_ids = pool_lang.search(cr, uid, [('code', '=', lang)])[0]
lang_obj = pool_lang.browse(cr, uid, lang_ids)
lang_dict = {
'lang_obj': lang_obj,
'date_format': lang_obj.date_format,
@ -76,7 +107,7 @@ class report(osv.Model):
self.default_lang[lang] = self.lang_dict.copy()
return True
def formatLang(self, value, digits=None, date=False, date_time=False, grouping=True, monetary=False, dp=False, currency_obj=False):
def formatLang(self, value, digits=None, date=False, date_time=False, grouping=True, monetary=False, dp=False, currency_obj=False, cr=None, uid=None):
"""
Assuming 'Account' decimal.precision=3:
formatLang(value) -> digits=2 (default)
@ -84,17 +115,20 @@ class report(osv.Model):
formatLang(value, dp='Account') -> digits=3
formatLang(value, digits=5, dp='Account') -> digits=5
"""
def get_date_length(date_format=DEFAULT_SERVER_DATE_FORMAT):
return len((datetime.now()).strftime(date_format))
if digits is None:
if dp:
digits = self.get_digits(dp=dp)
digits = self._get_digits(cr, uid, dp=dp)
else:
digits = self.get_digits(value)
digits = self._get_digits(cr, uid, value)
if isinstance(value, (str, unicode)) and not value:
return ''
if not self.lang_dict_called:
self._get_lang_dict()
self._get_lang_dict(cr, uid)
self.lang_dict_called = True
if date or date_time:
@ -117,9 +151,7 @@ class report(osv.Model):
date = datetime(*value.timetuple()[:6])
if date_time:
# Convert datetime values to the expected client/context timezone
date = datetime_field.context_timestamp(request.cr, request.uid,
timestamp=date,
context=self.localcontext)
date = datetime_field.context_timestamp(cr, uid, timestamp=date, context=self.localcontext)
return date.strftime(date_format.encode('utf-8'))
res = self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary)
@ -150,7 +182,7 @@ class report(osv.Model):
'tz': context.get('tz'),
'uid': context.get('uid'),
}
self._get_lang_dict()
self._get_lang_dict(cr, uid)
view_obj = self.pool['ir.ui.view']
@ -177,51 +209,180 @@ class report(osv.Model):
qcontext['o'] = self.pool[model].browse(cr, uid, doc_id, context=ctx)
return view_obj.render(cr, uid, template, qcontext, context=ctx)
current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
# Website independance code
website = False
res_company = current_user.company_id
try:
website = request.website
res_company = request.website.company_id
except:
pass
values.update({
'time': time,
'user': current_user,
'user_id': current_user.id,
'formatLang': self.formatLang,
'get_digits': self.get_digits,
'formatLang': partial(self.formatLang, cr=cr, uid=uid),
'get_digits': self._get_digits,
'render_doc': render_doc,
'website': website,
'res_company': res_company,
'editable': True, # Will active inherit_branding
'res_company': self.pool['res.users'].browse(cr, uid, uid).company_id,
'website': False, # Will be overidden by ir.ui.view if the request has website enabled
})
return view_obj.render(cr, uid, template, values, context=context)
def get_pdf(self, report, record_id, context=None):
"""Used to return the content of a generated PDF.
#--------------------------------------------------------------------------
# Main reports methods
#--------------------------------------------------------------------------
:returns: pdf
def get_html(self, cr, uid, ids, report_name, data=None, context=None):
"""This method generates and returns html version of a report.
"""
url = '/report/pdf/report/' + report.report_file + '/' + str(record_id)
reqheaders = Headers(request.httprequest.headers)
reqheaders.pop('Accept')
reqheaders.add('Accept', 'application/pdf')
reqheaders.pop('Content-Type')
reqheaders.add('Content-Type', 'text/plain')
response = Client(request.httprequest.app, BaseResponse).get(url, headers=reqheaders,
follow_redirects=True)
return response.data
# If the report is using a custom model to render its html, we must use it.
# Otherwise, fallback on the generic html rendering.
try:
report_model_name = 'report.%s' % report_name
particularreport_obj = self.pool[report_model_name]
return particularreport_obj.render_html(cr, uid, ids, data={'form': data}, context=context)
except KeyError:
report = self._get_report_from_name(cr, uid, report_name)
report_obj = self.pool[report.model]
docs = report_obj.browse(cr, uid, ids, context=context)
docargs = {
'doc_ids': ids,
'doc_model': report.model,
'docs': docs,
}
return self.render(cr, uid, [], report.report_name, docargs, context=context)
def get_pdf(self, cr, uid, ids, report_name, html=None, data=None, context=None):
"""This method generates and returns pdf version of a report.
"""
if context is None:
context = {}
if html is None:
html = self.get_html(cr, uid, ids, report_name, data=data, context=context)
html = html.decode('utf-8')
# Get the ir.actions.report.xml record we are working on.
report = self._get_report_from_name(cr, uid, report_name)
# Check attachment_use field. If set to true and an existing pdf is already saved, load
# this one now. Else, mark save it.
save_in_attachment = {}
if report.attachment_use is True:
save_in_attachment['model'] = report.model
save_in_attachment['loaded_documents'] = {}
for record_id in ids:
obj = self.pool[report.model].browse(cr, uid, record_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', '=', record_id)]
attach_ids = self.pool['ir.attachment'].search(cr, uid, alreadyindb)
if attach_ids:
# Add the loaded pdf in the loaded_documents list
pdf = self.pool['ir.attachment'].browse(cr, uid, attach_ids[0]).datas
pdf = base64.decodestring(pdf)
save_in_attachment['loaded_documents'][record_id] = pdf
_logger.info('The PDF document %s was loaded from the database' % filename)
else:
# Mark current document to be saved
save_in_attachment[id] = filename
# Get the paperformat associated to the report, otherwise fallback on the company one.
if not report.paperformat_id:
user = self.pool['res.users'].browse(cr, uid, uid)
paperformat = user.company_id.paperformat_id
else:
paperformat = report.paperformat_id
# Preparing the minimal html pages
#subst = self._get_url_content('/report/static/src/js/subst.js')[0] # Used in age numbering
subst = "<script src='/report/static/src/js/subst.js'></script> "
css = '' # Will contain local css
headerhtml = []
contenthtml = []
footerhtml = []
base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
minimalhtml = """
<base href="{base_url}">
<!DOCTYPE html>
<html style="height: 0;">
<head>
<link href="/report/static/src/css/reset.min.css" rel="stylesheet"/>
<link href="/web/static/lib/bootstrap/css/bootstrap.css" rel="stylesheet"/>
<link href="/website/static/src/css/website.css" rel="stylesheet"/>
<link href="/web/static/lib/fontawesome/css/font-awesome.css" rel="stylesheet"/>
<style type='text/css'>{css}</style>
{subst}
</head>
<body class="container" onload='subst()'>
{body}
</body>
</html>"""
# The retrieved html report must be simplified. We convert it into a xml tree
# via lxml in order to extract headers, footers and content.
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=css, subst=subst, body=body, base_url=base_url)
headerhtml.append(header)
for node in root.xpath("//div[@class='footer']"):
body = lxml.html.tostring(node)
footer = minimalhtml.format(css=css, subst=subst, body=body, base_url=base_url)
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 report's content. 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='%s']" % report.model)
if oemodelnode is not None:
reportid = oemodelnode.get('data-oe-id')
if reportid:
reportid = int(reportid)
else:
reportid = False
body = lxml.html.tostring(node)
reportcontent = minimalhtml.format(css=css, subst='', body=body, base_url=base_url)
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]
# Run wkhtmltopdf process
pdf = self._generate_wkhtml_pdf(
cr, uid, headerhtml, footerhtml, contenthtml, context.get('landscape'),
paperformat, specific_paperformat_args, save_in_attachment
)
return pdf
def get_action(self, cr, uid, ids, report_name, datas=None, context=None):
"""Used to return an action of type ir.actions.report.xml.
"""Return an action of type ir.actions.report.xml.
:param report_name: Name of the template to generate an action for
"""
# TODO: return the action for the ids passed in args
if context is None:
context = {}
@ -249,26 +410,203 @@ class report(osv.Model):
return action
def eval_params(self, dict_param):
"""Parse a dictionary generated by the webclient (javascript) into a dictionary
understandable by a wizard controller (python).
"""
for key, value in dict_param.iteritems():
if value.lower() == 'false':
dict_param[key] = False
elif value.lower() == 'true':
dict_param[key] = True
elif ',' in value:
dict_param[key] = [int(i) for i in value.split(',')]
elif '%2C' in value:
dict_param[key] = [int(i) for i in value.split('%2C')]
else:
try:
i = int(value)
dict_param[key] = i
except (ValueError, TypeError):
pass
#--------------------------------------------------------------------------
# Report generation helpers
#--------------------------------------------------------------------------
data = {}
data['form'] = dict_param
return data
def _check_wkhtmltopdf(self):
return wkhtmltopdf_state
def _generate_wkhtml_pdf(self, cr, uid, 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']
command_args = []
tmp_dir = tempfile.gettempdir()
# Passing the cookie to wkhtmltopdf in order to resolve URL.
try:
from openerp.addons.web.http import request
command_args.extend(['--cookie', 'session_id', request.session.sid])
except AttributeError:
pass
# Display arguments
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.flush()
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.flush()
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.flush()
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.flush()
try:
# If the server is running with only one worker, ask to create a secund to be able
# to serve the http request of wkhtmltopdf subprocess.
if config['workers'] == 1:
ppid = psutil.Process(os.getpid()).ppid
os.kill(ppid, signal.SIGTTIN)
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 config['workers'] == 1:
os.kill(ppid, signal.SIGTTOU)
if process.returncode != 0:
raise osv.except_osv(_('Report (PDF)'),
_('wkhtmltopdf 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],
}
self.pool['ir.attachment'].create(cr, uid, attachment)
_logger.info('The PDF document %s is now saved in the '
'database' % attachment['name'])
pdfreport.flush()
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 _get_report_from_name(self, cr, uid, report_name):
"""Get the first record of ir.actions.report.xml having the ``report_name`` as value for
the field report_name.
"""
report_obj = self.pool['ir.actions.report.xml']
qwebtypes = ['qweb-pdf', 'qweb-html']
conditions = [('report_type', 'in', qwebtypes), ('report_name', '=', report_name)]
idreport = report_obj.search(cr, uid, conditions)[0]
return report_obj.browse(cr, uid, idreport)
def _build_wkhtmltopdf_args(self, paperformat, specific_paperformat_args=None):
"""Build arguments understandable by wkhtmltopdf from a report.paperformat record.
:paperformat: report.paperformat record
:specific_paperformat_args: a dict containing prioritized wkhtmltopdf arguments
:returns: list of string representing 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'])])
elif paperformat.margin_top:
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 _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 = cStringIO.StringIO()
writer.write(merged)
merged.seek(0)
content = merged.read()
merged.close()
return content

View File

@ -1,4 +1,5 @@
openerp.report = function(instance) {
var wkhtmltopdf_state;
instance.web.ActionManager = instance.web.ActionManager.extend({
ir_actions_report_xml: function(action, options) {
@ -11,8 +12,7 @@ openerp.report = function(instance) {
// QWeb reports
if ('report_type' in action && (action.report_type == 'qweb-html' || action.report_type == 'qweb-pdf' || action.report_type == 'controller')) {
var report_url = ''
var report_url = '';
switch (action.report_type) {
case 'qweb-html':
report_url = '/report/' + action.report_name;
@ -49,41 +49,49 @@ openerp.report = function(instance) {
}
if (action.report_type == 'qweb-html') {
// Open the html report in a popup
window.open(report_url, '_blank', 'height=768,width=1024');
window.open(report_url, '_blank', 'height=900,width=1280');
instance.web.unblockUI();
return;
} else {
// Trigger the download of the pdf/custom controller report
// Trigger the download of the pdf/controller report
var c = openerp.webclient.crashmanager;
var response = new Array()
response[0] = report_url
response[1] = action.report_type
var response = new Array();
response[0] = report_url;
response[1] = action.report_type;
openerp.session.rpc('/report/check_wkhtmltopdf').then(function (presence) {
// Fallback of qweb-pdf if wkhtmltopdf is not installed
if (!presence && action.report_type == 'qweb-pdf') {
self.do_notify(_t('Report'), _t('Unable to find Wkhtmltopdf on this \
system. The report will be shown in html.<br><br><a href="http://wkhtmltopdf.org/" _target="blank">\
wkhtmltopdf.org</a>'), true);
window.open(report_url.substring(12), '_blank', 'height=768,width=1024');
instance.web.unblockUI();
}
else {
if (presence == 'upgrade') {
self.do_notify(_t('Report'), _t('You should upgrade your version of\
Wkhtmltopdf to at least 0.12.0 in order to get a correct display of headers and footers as well as\
support for table-breaking between pages.<br><br><a href="http://wkhtmltopdf.org/" \
target="_blank">wkhtmltopdf.org</a>'), true);
if (action.report_type == 'qweb-pdf') {
(wkhtmltopdf_state = wkhtmltopdf_state || openerp.session.rpc('/report/check_wkhtmltopdf')).then(function (presence) {
// Fallback of qweb-pdf if wkhtmltopdf is not installed
if (presence == 'install' && action.report_type == 'qweb-pdf') {
self.do_notify(_t('Report'), _t('Unable to find Wkhtmltopdf on this \
system. The report will be shown in html.<br><br><a href="http://wkhtmltopdf.org/" target="_blank">\
wkhtmltopdf.org</a>'), true);
window.open(report_url.substring(12), '_blank', 'height=768,width=1024');
instance.web.unblockUI();
} else {
if (presence == 'upgrade') {
self.do_notify(_t('Report'), _t('You should upgrade your version of\
Wkhtmltopdf to at least 0.12.0 in order to get a correct display of headers and footers as well as\
support for table-breaking between pages.<br><br><a href="http://wkhtmltopdf.org/" \
target="_blank">wkhtmltopdf.org</a>'), true);
}
self.session.get_file({
url: '/report/download',
data: {data: JSON.stringify(response)},
complete: openerp.web.unblockUI,
error: c.rpc_error.bind(c)
});
}
self.session.get_file({
url: '/report/download',
data: {data: JSON.stringify(response)},
complete: openerp.web.unblockUI,
error: c.rpc_error.bind(c)
});
}
});
}
});
} else {
self.session.get_file({
url: '/report/download',
data: {data: JSON.stringify(response)},
complete: openerp.web.unblockUI,
error: c.rpc_error.bind(c)
});
}
}
} else {
return self._super(action, options);
}

View File

@ -1,26 +1,36 @@
# -*- coding: utf-8 -*-
import logging
import logging
import openerp
import urllib2
_logger = logging.getLogger(__name__)
class TestReports(openerp.tests.HttpCase):
@openerp.tests.common.at_install(False)
@openerp.tests.common.post_install(True)
class TestReports(openerp.tests.TransactionCase):
def test_reports(self):
return # commented out until post_install tests are working
registry, cr, uid = self.registry, self.cr, self.uid
r_model = registry('ir.actions.report.xml')
domain = [('report_type','like','qweb')]
domain = [('report_type', 'like', 'qweb')]
for r in r_model.browse(cr, uid, r_model.search(cr, uid, domain)):
report_model = 'report.%s' % r.report_name
particular_model = registry('ir.model').search(cr, uid, [('model', '=', report_model)])
# Only test the generic reports here
if particular_model:
continue
_logger.info("testing report %s", r.report_name)
report_model = registry(r.model)
report_model_ids = report_model.search(cr, uid, [], limit=10)
if not report_model_ids:
_logger.info("no record found skipping report %s", r.report_name)
continue
if not r.multi:
report_model_ids = report_model_ids[:1]
url = "/report/%s/%s" % (r.report_name, ','.join(str(i) for i in report_model_ids))
_logger.info("testing report %s", url)
# TODO sle: uncomment this
#content = self.url_open(url)
# Test report generation
registry('report').get_html(cr, uid, report_model_ids, r.report_name)

View File

@ -4,9 +4,7 @@
<template id="html_container">
&lt;!DOCTYPE html&gt;
<html t-att-lang="lang and lang.replace('_', '-')"
t-att-data-website-id="website.id if editable else None"
t-att-data-editable="'1' if editable else None"
t-att-data-translatable="'1' if translatable else None"
t-att-data-view-xmlid="xmlid if editable else None"
t-att-data-main-object="repr(main_object) if editable else None">
<head>

View File

@ -85,6 +85,7 @@
class="oe_link"
string="Search associated QWeb views"
name="associated_view"
attrs="{'invisible':[('report_type', 'not in', ['qweb-pdf', 'qweb-html'])]}"
/>
</xpath>
</data>

View File

@ -91,7 +91,7 @@ class ir_http(orm.AbstractModel):
assert path is not None
except Exception:
return self._handle_exception(werkzeug.exceptions.NotFound())
if path != request.httprequest.path:
if path != werkzeug.url_quote(request.httprequest.path):
return werkzeug.utils.redirect(path)
def _handle_exception(self, exception=None, code=500):

View File

@ -381,42 +381,8 @@ class RelativeDatetime(orm.AbstractModel):
class Contact(orm.AbstractModel):
_name = 'website.qweb.field.contact'
_inherit = ['website.qweb.field', 'website.qweb.field.many2one']
_inherit = ['ir.qweb.field.contact', 'website.qweb.field.many2one']
def from_html(self, cr, uid, model, column, element, context=None):
# FIXME: this behavior is really weird, what if the user wanted to edit the name of the related thingy? Should m2os really be editable without a widget?
divs = element.xpath(".//div")
for div in divs:
if div != divs[0]:
div.getparent().remove(div)
return super(Contact, self).from_html(cr, uid, model, column, element, context=context)
def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
opf = options.get('fields') or ["name", "address", "phone", "mobile", "fax", "email"]
if not getattr(record, field_name):
return None
id = getattr(record, field_name).id
field_browse = self.pool[column._obj].browse(cr, openerp.SUPERUSER_ID, id, context={"show_address": True})
value = werkzeug.utils.escape( field_browse.name_get()[0][1] )
val = {
'name': value.split("\n")[0],
'address': werkzeug.utils.escape("\n".join(value.split("\n")[1:])),
'phone': field_browse.phone,
'mobile': field_browse.mobile,
'fax': field_browse.fax,
'city': field_browse.city,
'country_id': field_browse.country_id and field_browse.country_id.name_get()[0][1],
'email': field_browse.email,
'fields': opf,
'options': options
}
html = self.pool["ir.ui.view"].render(cr, uid, "website.contact", val, engine='website.qweb', context=context).decode('utf8')
return ir_qweb.HTMLSafe(html)
def html_to_text(element):
""" Converts HTML content with HTML-specified line breaks (br, p, div, ...)

View File

@ -760,23 +760,5 @@ Sitemap: <t t-esc="url_root"/>sitemap.xml
</t>
</template>
<template id="contact">
<address t-ignore="true" class="mb0" itemscope="itemscope" itemtype="http://schema.org/Organization">
<div t-att-class="'name' not in fields and 'css_non_editable_mode_hidden'"><span itemprop="name" t-esc="name"/></div>
<div itemprop="address" itemscope="itemscope" itemtype="http://schema.org/PostalAddress">
<div t-if="address and 'address' in fields" class='css_editable_mode_hidden'>
<i t-if="not options.get('no_marker')" class='fa fa-map-marker'/> <span itemprop="streetAddress" t-raw="address.replace('\n', options.get('no_tag_br') and ', ' or ('&lt;br/&gt;%s' % ('' if options.get('no_marker') else '&amp;nbsp; &amp;nbsp; ')))"/>
</div>
<div t-if="city and 'city' in fields" class='css_editable_mode_hidden'>
<i t-if="not options.get('no_marker')" class='fa fa-map-marker'/> <span itemprop="addressLocality" t-raw="city"/>, <span itemprop="addressCountry" t-raw="country_id"/>
</div>
<div t-if="phone and 'phone' in fields" class='css_editable_mode_hidden'><i t-if="not options.get('no_marker')" class='fa fa-phone'/> <span itemprop="telephone" t-esc="phone"/></div>
<div t-if="mobile and 'mobile' in fields" class='css_editable_mode_hidden'><i t-if="not options.get('no_marker')" class='fa fa-mobile-phone'/> <span itemprop="telephone" t-esc="mobile"/></div>
<div t-if="fax and 'fax' in fields" class='css_editable_mode_hidden'><i t-if="not options.get('no_marker')" class='fa fa-file-text-o'/> <span itemprop="faxNumber" t-esc="fax"/></div>
<div t-if="email and 'email' in fields" class='css_editable_mode_hidden'><i t-if="not options.get('no_marker')" class='fa fa-envelope'/> <span itemprop="email" t-esc="email"/></div>
</div>
</address>
</template>
</data>
</openerp>

View File

@ -2,6 +2,11 @@
<openerp>
<data>
<template id="editor_head" inherit_id="report.html_container" name="Editor" groups="base.group_website_publisher">
<xpath expr="//html" position="attributes">
<attribute name="t-att-data-website-id">website.id if editable and website else None</attribute>
<attribute name="t-att-data-translatable">'1' if translatable else None</attribute>
</xpath>
<xpath expr="//body" position="attributes">
<attribute name="style">padding-top: 51px;</attribute>
</xpath>
@ -42,16 +47,18 @@
<script type="text/javascript" src="/web/static/lib/select2/select2.js"></script>
<script type="text/javascript" src="/web/static/lib/ckeditor/ckeditor.js"></script>
<script type="text/javascript" src="/website/static/lib/bootstrap-tour/bootstrap-tour.js"></script>
<script t-if="not translatable" type="text/javascript" src="/website/static/lib/ace/ace.js"></script>
<script type="text/javascript" src="/website/static/lib/vkbeautify/vkbeautify.0.99.00.beta.js"></script>
<script type="text/javascript" src="/web/static/lib/jquery.ui/js/jquery-ui-1.9.1.custom.js"></script>
<script type="text/javascript" src="/website/static/lib/bootstrap-tour/bootstrap-tour.js"></script>
<!-- mutation observers shim backed by mutation events (8 < IE < 11, Safari < 6, FF < 14, Chrome < 17) -->
<script type="text/javascript" src="/website/static/lib//jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js"></script>
<script type="text/javascript" src="/website/static/lib/MutationObservers/test/sidetable.js"></script>
<script type="text/javascript" src='/website/static/lib/nearest/jquery.nearest.js'></script>
<script type="text/javascript" src="/website/static/lib/MutationObservers/MutationObserver.js"></script>
<script type="text/javascript" src="/website/static/lib/jquery.placeholder/jquery.placeholder.js"></script>
<script type="text/javascript" src="/website/static/src/js/website.editor.js"></script>
<script type="text/javascript" src="/website/static/src/js/website.editor.newpage.js" groups="base.group_website_designer"></script>
<script type="text/javascript" src="/website/static/src/js/website.menu.js" groups="base.group_website_designer"></script>
@ -65,7 +72,7 @@
</xpath>
<xpath expr='//body[@class="container"]/div[@id="wrapwrap"]' position="before">
<t t-set="languages" t-value="website.get_languages()"/>
<t t-set="languages" t-value="website.get_languages() if website else list()"/>
<ul class="list-inline js_language_selector mt16" t-if="(len(languages) &gt; 1 or editable)">
<li t-foreach="languages" t-as="lg">
<a t-att-href="url_for('', lang=lg[0]) + '?' + keep_query()"