summaryrefslogtreecommitdiff
path: root/odoo
diff options
context:
space:
mode:
authorLaurent Smet <las@openerp.com>2017-04-26 09:31:55 +0200
committerLaurent Smet <las@openerp.com>2017-05-08 09:23:12 +0200
commite80238042c9d93d492ea8b06b0041aced0d81dcd (patch)
tree70ed26eb99cf62ff7d5418a0748d1aa166136067 /odoo
parent3425752eaca236c1e4b4d616aa49b3faa6cfd957 (diff)
[REF] report: remove the report module
The content of the report module is now dispatched in the 'base' and the 'web' modules.
Diffstat (limited to 'odoo')
-rw-r--r--odoo/addons/base/__init__.py1
-rw-r--r--odoo/addons/base/__manifest__.py1
-rw-r--r--odoo/addons/base/base_data.xml48
-rw-r--r--odoo/addons/base/base_demo.xml2
-rw-r--r--odoo/addons/base/ir/__init__.py1
-rw-r--r--odoo/addons/base/ir/ir_actions.py94
-rw-r--r--odoo/addons/base/ir/ir_actions.xml6
-rw-r--r--odoo/addons/base/ir/ir_actions_report.py711
-rw-r--r--odoo/addons/base/ir/ir_model_report.xml1
-rw-r--r--odoo/addons/base/ir/ir_qweb/fields.py22
-rw-r--r--odoo/addons/base/ir/report_ir_model.xml2
-rw-r--r--odoo/addons/base/module/module_report.xml1
-rw-r--r--odoo/addons/base/module/report/ir_module_reference_print.py8
-rw-r--r--odoo/addons/base/module/report/report_ir_module_reference.xml4
-rw-r--r--odoo/addons/base/res/__init__.py1
-rw-r--r--odoo/addons/base/res/report_paperformat.py68
-rw-r--r--odoo/addons/base/res/report_paperformat_views.xml106
-rw-r--r--odoo/addons/base/res/res_company.py34
-rw-r--r--odoo/addons/base/res/res_company_view.xml46
-rw-r--r--odoo/addons/base/security/ir.model.access.csv2
-rw-r--r--odoo/addons/base/static/img/bg_background_template.jpgbin0 -> 192515 bytes
-rw-r--r--odoo/addons/base/static/img/logo_sample.pngbin0 -> 4303 bytes
-rw-r--r--odoo/addons/base/static/img/preview_background.pngbin0 -> 107999 bytes
-rw-r--r--odoo/addons/base/static/img/preview_boxed.pngbin0 -> 37257 bytes
-rw-r--r--odoo/addons/base/static/img/preview_clean.pngbin0 -> 32792 bytes
-rw-r--r--odoo/addons/base/static/img/preview_standard.pngbin0 -> 39737 bytes
-rw-r--r--odoo/addons/base/static/pdf/preview_background.pdf0
-rw-r--r--odoo/addons/base/static/pdf/preview_boxed.pdf0
-rw-r--r--odoo/addons/base/static/pdf/preview_clean.pdf0
-rw-r--r--odoo/addons/base/static/pdf/preview_standard.pdf0
-rw-r--r--odoo/addons/base/tests/__init__.py1
-rw-r--r--odoo/addons/base/tests/test_reports.py34
-rw-r--r--odoo/tools/yaml_import.py4
33 files changed, 1095 insertions, 103 deletions
diff --git a/odoo/addons/base/__init__.py b/odoo/addons/base/__init__.py
index 12f085fadc9..2c91802f59c 100644
--- a/odoo/addons/base/__init__.py
+++ b/odoo/addons/base/__init__.py
@@ -4,7 +4,6 @@
from . import ir
from . import module
from . import res
-from . import report
def post_init(cr, registry):
"""Rewrite ICP's to force groups"""
diff --git a/odoo/addons/base/__manifest__.py b/odoo/addons/base/__manifest__.py
index 6510f6c1987..a34c2804356 100644
--- a/odoo/addons/base/__manifest__.py
+++ b/odoo/addons/base/__manifest__.py
@@ -61,6 +61,7 @@ The kernel of Odoo, needed for all installation.
'res/res_partner_data.xml',
'res/ir_property_view.xml',
'res/res_security.xml',
+ 'res/report_paperformat_views.xml',
'security/ir.model.access.csv',
],
'demo': [
diff --git a/odoo/addons/base/base_data.xml b/odoo/addons/base/base_data.xml
index 7c90d0b96e9..94ec6672cd1 100644
--- a/odoo/addons/base/base_data.xml
+++ b/odoo/addons/base/base_data.xml
@@ -83,5 +83,53 @@ Administrator</span>]]></field>
<field name="active" eval="False"/>
</record>
+ <record id="paperformat_euro" model="report.paperformat">
+ <field name="name">European A4</field>
+ <field name="default" eval="True" />
+ <field name="format">A4</field>
+ <field name="page_height">0</field>
+ <field name="page_width">0</field>
+ <field name="orientation">Portrait</field>
+ <field name="margin_top">40</field>
+ <field name="margin_bottom">28</field>
+ <field name="margin_left">7</field>
+ <field name="margin_right">7</field>
+ <field name="header_line" eval="False" />
+ <field name="header_spacing">35</field>
+ <field name="dpi">90</field>
+ </record>
+
+ <record id="paperformat_us" model="report.paperformat">
+ <field name="name">US Letter</field>
+ <field name="default" eval="True" />
+ <field name="format">Letter</field>
+ <field name="page_height">0</field>
+ <field name="page_width">0</field>
+ <field name="orientation">Portrait</field>
+ <field name="margin_top">40</field>
+ <field name="margin_bottom">25</field>
+ <field name="margin_left">7</field>
+ <field name="margin_right">7</field>
+ <field name="header_line" eval="False" />
+ <field name="header_spacing">35</field>
+ <field name="dpi">90</field>
+ </record>
+
+ <record id="paperformat_batch_deposit" model="report.paperformat">
+ <field name="name">US Batch Deposit</field>
+ <field name="default" eval="False" />
+ <field name="format">Letter</field>
+ <field name="page_height">0</field>
+ <field name="page_width">0</field>
+ <field name="orientation">Portrait</field>
+ <field name="margin_top">15</field>
+ <field name="margin_bottom">10</field>
+ <field name="margin_left">10</field>
+ <field name="margin_right">10</field>
+ <field name="header_line" eval="False" />
+ <field name="header_spacing">0</field>
+ <field name="dpi">90</field>
+ </record>
+
</data>
</odoo>
diff --git a/odoo/addons/base/base_demo.xml b/odoo/addons/base/base_demo.xml
index baf25fce38d..ccc8b15eaab 100644
--- a/odoo/addons/base/base_demo.xml
+++ b/odoo/addons/base/base_demo.xml
@@ -31,6 +31,7 @@
<record id="main_company" model="res.company">
<field name="name">YourCompany</field>
+ <field name="external_report_layout">standard</field>
</record>
<record id="user_demo" model="res.users">
@@ -80,6 +81,5 @@
<record id="base.group_portal" model="res.groups"><!-- Add the demo user to the portal (and therefore to the portal member group) -->
<field name="users" eval="[(4,ref('demo_user0'))]"/>
</record>
-
</data>
</odoo>
diff --git a/odoo/addons/base/ir/__init__.py b/odoo/addons/base/ir/__init__.py
index 0b99d65f2bf..747575c7bf4 100644
--- a/odoo/addons/base/ir/__init__.py
+++ b/odoo/addons/base/ir/__init__.py
@@ -6,6 +6,7 @@ from . import ir_sequence
from . import ir_ui_menu
from . import ir_ui_view
from . import ir_actions
+from . import ir_actions_report
from . import ir_attachment
from . import ir_cron
from . import ir_filters
diff --git a/odoo/addons/base/ir/ir_actions.py b/odoo/addons/base/ir/ir_actions.py
index 959b9b3f740..918e5d90880 100644
--- a/odoo/addons/base/ir/ir_actions.py
+++ b/odoo/addons/base/ir/ir_actions.py
@@ -1,17 +1,18 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import odoo
+from odoo import api, fields, models, tools, SUPERUSER_ID, _
+from odoo.exceptions import MissingError, UserError, ValidationError, AccessError
+from odoo.tools.safe_eval import safe_eval, test_python_expr
+from odoo.http import request
+
import datetime
import dateutil
import logging
-import os
import time
-from pytz import timezone
-import odoo
-from odoo import api, fields, models, tools, _
-from odoo.exceptions import MissingError, UserError, ValidationError
-from odoo.tools.safe_eval import safe_eval, test_python_expr
+from pytz import timezone
_logger = logging.getLogger(__name__)
@@ -71,87 +72,6 @@ class IrActions(models.Model):
}
-class IrActionsReport(models.Model):
- _name = 'ir.actions.report'
- _inherit = 'ir.actions.actions'
- _table = 'ir_act_report_xml'
- _sequence = 'ir_actions_id_seq'
- _order = 'name'
-
- name = fields.Char(translate=True)
- type = fields.Char(default='ir.actions.report')
-
- model = fields.Char(required=True)
- report_type = fields.Selection([('qweb-pdf', 'PDF'), ('qweb-html', 'HTML')], required=True, default="pdf",
- help="HTML will open the report directly in your browser, PDF will use wkhtmltopdf to render the HTML into a PDF file and let you download it, Controller allows you to define the url of a custom controller outputting any kind of report.")
- report_name = fields.Char(string='Template Name', required=True,
- help="For QWeb reports, name of the template used in the rendering. The method 'render_html' of the model 'report.template_name' will be called (if any) to give the html. For RML reports, this is the LocalService name.")
- groups_id = fields.Many2many('res.groups', 'res_groups_report_rel', 'uid', 'gid', string='Groups')
- ir_values_id = fields.Many2one('ir.values', string='More Menu entry', readonly=True,
- help='More menu entry.', copy=False)
-
- # options
- multi = fields.Boolean(string='On Multiple Doc.', help="If set to true, the action will not be displayed on the right toolbar of a form view.")
- attachment_use = fields.Boolean(string='Reload from Attachment', help='If you check this, then the second time the user prints with same attachment name, it returns the previous report.')
- attachment = fields.Char(string='Save as Attachment Prefix',
- help='This is the filename of the attachment used to store the printing result. Keep empty to not save the printed reports. You can use a python expression with the object and time variables.')
-
- # Deprecated rml stuff
- header = fields.Boolean(string='Add RML Header', default=True, help="Add or not the corporate RML header")
- parser = fields.Char(string='Parser Class')
- auto = fields.Boolean(string='Custom Python Parser', default=True)
-
- report_file = fields.Char(string='Report File', required=False, readonly=False, store=True,
- help="The path to the main report file (depending on Report Type) or empty if the content is in another field")
-
- @api.multi
- def create_action(self):
- """ Create a contextual action for each report. """
- for report in self:
- ir_values = self.env['ir.values'].sudo().create({
- 'name': report.name,
- 'model': report.model,
- 'key2': 'client_print_multi',
- 'value': "ir.actions.report,%s" % report.id,
- })
- report.write({'ir_values_id': ir_values.id})
- return True
-
- @api.multi
- def unlink_action(self):
- """ Remove the contextual actions created for the reports. """
- self.check_access_rights('write', raise_exception=True)
- for report in self:
- if report.ir_values_id:
- try:
- report.ir_values_id.sudo().unlink()
- except Exception:
- raise UserError(_('Deletion of the action record failed.'))
- return True
-
- @api.model
- def render_html(self, res_ids, data=None):
- return self.env['report'].get_html(res_ids, self.report_name, data=data), 'html'
-
- @api.multi
- def render_pdf(self, res_ids, data=None):
- # In case of test environment without enough workers to perform calls to wkhtmltopdf,
- # fallback to render_html.
- if tools.config['test_enable'] and not tools.config['test_report_directory']:
- return self.render_html(res_ids, data=data)
- return self.env['report'].get_pdf(res_ids, self.report_name, data=data), 'pdf'
-
- @api.multi
- def render(self, res_ids, data=None):
- report_type = self.report_type.lower()
- if report_type.startswith('qweb-'):
- report_type = report_type[5:]
- render_func = getattr(self, 'render_' + report_type, None)
- if not render_func:
- return None
- return render_func(res_ids, data=data)
-
-
class IrActionsActWindow(models.Model):
_name = 'ir.actions.act_window'
_table = 'ir_act_window'
diff --git a/odoo/addons/base/ir/ir_actions.xml b/odoo/addons/base/ir/ir_actions.xml
index 1d26521bb56..fda81ef402e 100644
--- a/odoo/addons/base/ir/ir_actions.xml
+++ b/odoo/addons/base/ir/ir_actions.xml
@@ -61,15 +61,21 @@
<button name="unlink_action" string="Remove from the 'Print' menu" type="object"
attrs="{'invisible':[('ir_values_id','=',False)]}" icon="fa-minus-square"
help="Remove the contextual action related this report" class="oe_stat_button"/>
+ <button name="associated_view" string="QWeb views" type="object"
+ attrs="{'invisible':[('report_type', 'not in', ['qweb-pdf', 'qweb-html'])]}" icon='fa-code'
+ class="oe_stat_button"/>
</div>
<group>
<group>
<field name="name"/>
<field name="report_type"/>
+ <field name="paperformat_id"
+ attrs="{'invisible':[('report_type','not in',['qweb-pdf'])]}"/>
</group>
<group>
<field name="model"/>
<field name="report_name"/>
+ <field name="print_report_name" />
</group>
</group>
<notebook>
diff --git a/odoo/addons/base/ir/ir_actions_report.py b/odoo/addons/base/ir/ir_actions_report.py
new file mode 100644
index 00000000000..21777901528
--- /dev/null
+++ b/odoo/addons/base/ir/ir_actions_report.py
@@ -0,0 +1,711 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo import api, fields, models, tools, SUPERUSER_ID, _
+from odoo.exceptions import MissingError, UserError, ValidationError, AccessError
+from odoo.tools.safe_eval import safe_eval, test_python_expr
+from odoo.tools.misc import find_in_path
+from odoo.tools import config
+from odoo.sql_db import TestCursor
+from odoo.http import request
+
+import time
+import base64
+import logging
+import os
+import lxml.html
+import tempfile
+import subprocess
+import re
+
+from collections import namedtuple
+from contextlib import closing
+from pyPdf import PdfFileWriter, PdfFileReader
+from distutils.version import LooseVersion
+from reportlab.graphics.barcode import createBarcodeDrawing
+
+
+_logger = logging.getLogger(__name__)
+
+
+WkhtmltopdfObj = namedtuple('WkhtmltopdfObj',
+ ['header', 'content', 'footer', 'res_id', 'attachment_id', 'attachment_name'])
+
+
+# A lock occurs when the user wants to print a report having multiple barcode while the server is
+# started in threaded-mode. The reason is that reportlab has to build a cache of the T1 fonts
+# before rendering a barcode (done in a C extension) and this part is not thread safe. We attempt
+# here to init the T1 fonts cache at the start-up of Odoo so that rendering of barcode in multiple
+# thread does not lock the server.
+try:
+ createBarcodeDrawing('Code128', value='foo', format='png', width=100, height=100, humanReadable=1).asString('png')
+except Exception:
+ pass
+
+def _get_wkhtmltopdf_bin():
+ return find_in_path('wkhtmltopdf')
+
+
+# Check the presence of Wkhtmltopdf and return its version at Odoo start-up
+wkhtmltopdf_state = 'install'
+try:
+ process = subprocess.Popen(
+ [_get_wkhtmltopdf_bin(), '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE
+ )
+except (OSError, IOError):
+ _logger.info('You need Wkhtmltopdf to print a pdf version of the reports.')
+else:
+ _logger.info('Will use the Wkhtmltopdf binary at %s' % _get_wkhtmltopdf_bin())
+ out, err = process.communicate()
+ match = re.search('([0-9.]+)', out)
+ if match:
+ version = match.group(0)
+ if LooseVersion(version) < LooseVersion('0.12.0'):
+ _logger.info('Upgrade Wkhtmltopdf to (at least) 0.12.0')
+ wkhtmltopdf_state = 'upgrade'
+ else:
+ wkhtmltopdf_state = 'ok'
+
+ if config['workers'] == 1:
+ _logger.info('You need to start Odoo with at least two workers to print a pdf version of the reports.')
+ wkhtmltopdf_state = 'workers'
+ else:
+ _logger.info('Wkhtmltopdf seems to be broken.')
+ wkhtmltopdf_state = 'broken'
+
+
+def _merge_pdf(documents):
+ '''Merge PDF files into one.
+
+ :param documents: list of path of pdf files
+ :returns: path of the merged pdf
+ '''
+ writer = PdfFileWriter()
+ streams = [] # We have to close the streams *after* PdfFilWriter's call to write()
+ for document in documents:
+ pdfreport = file(document, 'rb')
+ streams.append(pdfreport)
+ reader = PdfFileReader(pdfreport)
+ for page in range(0, reader.getNumPages()):
+ writer.addPage(reader.getPage(page))
+
+ merged_file_fd, merged_file_path = tempfile.mkstemp(suffix='.html', prefix='report.merged.tmp.')
+ with closing(os.fdopen(merged_file_fd, 'w')) as merged_file:
+ writer.write(merged_file)
+
+ for stream in streams:
+ stream.close()
+
+ return merged_file_path
+
+
+class IrActionsReport(models.Model):
+ _name = 'ir.actions.report'
+ _inherit = 'ir.actions.actions'
+ _table = 'ir_act_report_xml'
+ _sequence = 'ir_actions_id_seq'
+ _order = 'name'
+
+ name = fields.Char(translate=True)
+ type = fields.Char(default='ir.actions.report')
+ model = fields.Char(required=True)
+
+ report_type = fields.Selection([('qweb-html', 'HTML'), ('qweb-pdf', 'PDF')], required=True, default='qweb-pdf',
+ help='The type of the report that will be rendered, each one having its own rendering method.'
+ 'HTML means the report will be opened directly in your browser'
+ 'PDF means the report will be rendered using Wkhtmltopdf and downloaded by the user.')
+ report_name = fields.Char(string='Template Name', required=True,
+ help="For QWeb reports, name of the template used in the rendering. The method 'render_html' of the model 'report.template_name' will be called (if any) to give the html. For RML reports, this is the LocalService name.")
+ report_file = fields.Char(string='Report File', required=False, readonly=False, store=True,
+ help="The path to the main report file (depending on Report Type) or empty if the content is in another field")
+ groups_id = fields.Many2many('res.groups', 'res_groups_report_rel', 'uid', 'gid', string='Groups')
+ ir_values_id = fields.Many2one('ir.values', string='More Menu entry', readonly=True,
+ help='More menu entry.', copy=False)
+ multi = fields.Boolean(string='On Multiple Doc.', help="If set to true, the action will not be displayed on the right toolbar of a form view.")
+
+ paperformat_id = fields.Many2one('report.paperformat', 'Paper format')
+ print_report_name = fields.Char('Printed Report Name',
+ help="This is the filename of the report going to download. Keep empty to not change the report filename. You can use a python expression with the object and time variables.")
+ attachment_use = fields.Boolean(string='Reload from Attachment',
+ help='If you check this, then the second time the user prints with same attachment name, it returns the previous report.')
+ attachment = fields.Char(string='Save as Attachment Prefix',
+ help='This is the filename of the attachment used to store the printing result. Keep empty to not save the printed reports. You can use a python expression with the object and time variables.')
+
+
+ @api.multi
+ def associated_view(self):
+ """Used in the ir.actions.report form view in order to search naively after the view(s)
+ used in the rendering.
+ """
+ self.ensure_one()
+ action_ref = self.env.ref('base.action_ui_view')
+ if not action_ref or len(self.report_name.split('.')) < 2:
+ return False
+ action_data = action_ref.read()[0]
+ action_data['domain'] = [('name', 'ilike', self.report_name.split('.')[1]), ('type', '=', 'qweb')]
+ return action_data
+
+ @api.multi
+ def create_action(self):
+ """ Create a contextual action for each report. """
+ for report in self:
+ ir_values = self.env['ir.values'].sudo().create({
+ 'name': report.name,
+ 'model': report.model,
+ 'key2': 'client_print_multi',
+ 'value': "ir.actions.report,%s" % report.id,
+ })
+ report.write({'ir_values_id': ir_values.id})
+ return True
+
+ @api.multi
+ def unlink_action(self):
+ """ Remove the contextual actions created for the reports. """
+ self.check_access_rights('write', raise_exception=True)
+ for report in self:
+ if report.ir_values_id:
+ try:
+ report.ir_values_id.sudo().unlink()
+ except Exception:
+ raise UserError(_('Deletion of the action record failed.'))
+ return True
+
+ #--------------------------------------------------------------------------
+ # Main report methods
+ #--------------------------------------------------------------------------
+ @api.model
+ def retrieve_attachment(self, record_id, attachment_name=None):
+ '''Retrieve an attachment for a specific record.
+
+ :param res_id: The record_id.
+ :param attachment_name: The name of the attachment.
+ :return: A recordset of length <= 1
+ '''
+ if not attachment_name:
+ attachment_name = safe_eval(self.attachment, {'object': record_id, 'time': time})
+ return self.env['ir.attachment'].search([
+ ('datas_fname', '=', attachment_name),
+ ('res_model', '=', self.model),
+ ('res_id', '=', record_id.id)
+ ], limit=1)
+
+ @api.model
+ def create_wkhtmltopdf_obj(self, header, content, footer, res_id=None):
+ '''Create an object using namedtuple that represents a "sub-report" in wkhtmltopdf.
+ This object contains header, content, footer, res_id and data related to the attachment:
+ * attachment_id: an existing attachment_id found for the record.
+ * attachment_name: the expected name of the attachment created (if necessary) after calling wkhtmltopdf.
+
+ :param header: The header as a string.
+ :param content: The content as a string.
+ :param footer: The footer as a string.
+ :param res_id: The related record of the report.
+ :return: A new instance of WkhtmltopdfObj.
+ '''
+ attachment_id = attachment_name = None
+ if res_id and len(self._ids) == 1 and self.attachment_use and self.attachment:
+ record_id = self.env[self.model].browse(res_id)
+ attachment_name = safe_eval(self.attachment, {'object': record_id, 'time': time})
+ attachment_id = self.retrieve_attachment(record_id, attachment_name)
+ return WkhtmltopdfObj(
+ header=header,
+ content=content,
+ footer=footer,
+ res_id=res_id,
+ attachment_id=attachment_id,
+ attachment_name=attachment_name
+ )
+
+ @api.model
+ def postprocess_pdf_report(self, res_id, attachment_content, attachment_name):
+ '''Hook to handle post processing during the pdf report generation.
+ The basic behavior consists to create a new attachment containing the pdf
+ base64 encoded.
+
+ :param res_id: The record id.
+ :param attachment_content: The pdf content newly generated by wkhtmltopdf.
+ :param attachment_name: The name of the attachment.
+ '''
+ attachment = {
+ 'name': attachment_name,
+ 'datas': base64.encodestring(attachment_content),
+ 'datas_fname': attachment_name,
+ 'res_model': self.model,
+ 'res_id': res_id,
+ }
+ try:
+ self.env['ir.attachment'].create(attachment)
+ except AccessError:
+ _logger.info("Cannot save PDF report %r as attachment", attachment['name'])
+ else:
+ _logger.info('The PDF document %s is now saved in the database', attachment['name'])
+
+ @api.model
+ def get_wkhtmltopdf_state(self):
+ '''Get the current state of wkhtmltopdf: install, ok, upgrade, workers or broken.
+ * install: Starting state.
+ * upgrade: The binary is an older version (< 0.12.0).
+ * ok: A binary was found with a recent version (>= 0.12.0).
+ * workers: Not enough workers found to perform the pdf rendering process (< 2 workers).
+ * broken: A binary was found but not responding.
+
+ :return: wkhtmltopdf_state
+ '''
+ return wkhtmltopdf_state
+
+ @api.model
+ def _build_wkhtmltopdf_args(
+ self,
+ paperformat,
+ landscape,
+ specific_paperformat_args=None,
+ set_viewport_size=False):
+ '''Build arguments understandable by wkhtmltopdf bin.
+
+ :param paperformat: A report.paperformat record.
+ :param landscape: Force the report orientation to be landscape.
+ :param specific_paperformat_args: A dictionary containing prioritized wkhtmltopdf arguments.
+ :param set_viewport_size: Enable a viewport sized '1024x1280' or '1280x1024' depending of landscape arg.
+ :return: A list of string representing the wkhtmltopdf arguments.
+ '''
+ command_args = []
+ if set_viewport_size:
+ command_args.extend(['--viewport-size', landscape and '1024x1280' or '1280x1024'])
+
+ # Passing the cookie to wkhtmltopdf in order to resolve internal links.
+ try:
+ if request:
+ command_args.extend(['--cookie', 'session_id', request.session.sid])
+ except AttributeError:
+ pass
+
+ # Less verbose error messages
+ command_args.extend(['--quiet'])
+
+ # Build paperformat args
+ if paperformat:
+ 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) + 'mm'])
+ command_args.extend(['--page-height', str(paperformat.page_height) + 'mm'])
+
+ if specific_paperformat_args and specific_paperformat_args.get('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 specific_paperformat_args and specific_paperformat_args.get('data-report-dpi'):
+ command_args.extend(['--dpi', str(specific_paperformat_args['data-report-dpi'])])
+ elif paperformat.dpi:
+ if os.name == 'nt' and int(paperformat.dpi) <= 95:
+ _logger.info("Generating PDF on Windows platform require DPI >= 96. Using 96 instead.")
+ command_args.extend(['--dpi', '96'])
+ else:
+ command_args.extend(['--dpi', str(paperformat.dpi)])
+
+ if specific_paperformat_args and specific_paperformat_args.get('data-report-header-spacing'):
+ command_args.extend(['--header-spacing', str(specific_paperformat_args['data-report-header-spacing'])])
+ elif paperformat.header_spacing:
+ command_args.extend(['--header-spacing', str(paperformat.header_spacing)])
+
+ command_args.extend(['--margin-left', str(paperformat.margin_left)])
+ command_args.extend(['--margin-bottom', str(paperformat.margin_bottom)])
+ command_args.extend(['--margin-right', str(paperformat.margin_right)])
+ if not landscape and paperformat.orientation:
+ command_args.extend(['--orientation', str(paperformat.orientation)])
+ if paperformat.header_line:
+ command_args.extend(['--header-line'])
+
+ if landscape:
+ command_args.extend(['--orientation', 'landscape'])
+
+ return command_args
+
+ @api.model
+ def _extract_wkhtmltopdf_data_from_html(self, res_ids, html):
+ '''Extract information from the html passed as parameter and returns it as
+ a dictionary.
+
+ :param res_ids: The records ids.
+ :param html: The html as a string.
+ :return: data found in the html as a dictionary.
+ '''
+ IrConfig = self.env['ir.config_parameter'].sudo()
+ headers = []
+ contents = []
+ footers = []
+ ids = []
+ base_url = IrConfig.get_param('report.url') or IrConfig.get_param('web.base.url')
+
+ # Return empty dictionary if 'web.minimal_layout' not found.
+ layout = self.env.ref('web.minimal_layout', False)
+ if not layout:
+ return {}
+ layout = self.env['ir.ui.view'].browse(self.env['ir.ui.view'].get_view_id('web.minimal_layout'))
+
+ root = lxml.html.fromstring(html)
+ match_klass = "//div[contains(concat(' ', normalize-space(@class), ' '), ' {} ')]"
+
+ # Retrieve headers
+ for node in root.xpath(match_klass.format('header')):
+ body = lxml.html.tostring(node)
+ header = layout.render(dict(subst=True, body=body, base_url=base_url))
+ headers.append(header)
+
+ # Retrieve footers
+ for node in root.xpath(match_klass.format('footer')):
+ body = lxml.html.tostring(node)
+ footer = layout.render(dict(subst=True, body=body, base_url=base_url))
+ footers.append(footer)
+
+ # Retrieve content & ids
+ for node in root.xpath(match_klass.format('article')):
+ # 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
+ if res_ids and len(res_ids) == 1:
+ report_id = res_ids[0]
+ else:
+ oemodelnode = node.find(".//*[@data-oe-model='%s']" % self.model)
+ if oemodelnode is not None:
+ report_id = oemodelnode.get('data-oe-id')
+ if report_id:
+ report_id = int(report_id)
+ else:
+ report_id = False
+
+ # Extract the body
+ body = lxml.html.tostring(node)
+ content = layout.render(dict(subst=False, body=body, base_url=base_url))
+
+ contents.append(content)
+ ids.append(report_id)
+
+ # Create a list of wkhtmltopdf_objs, each one representing a "sub-report".
+ wkhtmltopdf_objs = []
+ for i in range(0, len(contents)):
+ header = headers[i] if headers else None
+ footer = footers[i] if footers else None
+ wkhtmltopdf_obj = self.create_wkhtmltopdf_obj(header, contents[i], footer, ids[i])
+ wkhtmltopdf_objs.append(wkhtmltopdf_obj)
+
+ # 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]
+
+ return {
+ 'wkhtmltopdf_objs': wkhtmltopdf_objs,
+ 'specific_paperformat_args': specific_paperformat_args,
+ }
+
+ @api.model
+ def _run_wkhtmltopdf(
+ self,
+ wkhtmltopdf_objs,
+ landscape,
+ paperformat,
+ specific_paperformat_args=None,
+ set_viewport_size=False):
+ '''Execute wkhtmltopdf as a subprocess in order to convert html given in input into a pdf
+ document.
+
+ :param wkhtmltopdf_objs: A list of WkhtmltopdfObj generated by the method create_wkhtmltopdf_obj in ir.actions.report
+ :param landscape: 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 set_viewport_size: Enable a viewport sized '1024x1280' or '1280x1024' depending of landscape arg.
+ :param postprocess_method: A method that will be called for each "sub-report" to perform a post processing like
+ the generation of report attachments.
+ :return: Content of the pdf as a string
+ '''
+ # Build the base command args for wkhtmltopdf bin
+ command_args = self._build_wkhtmltopdf_args(
+ paperformat,
+ landscape,
+ specific_paperformat_args=specific_paperformat_args,
+ set_viewport_size=set_viewport_size)
+
+ # Execute WKhtmltopdf
+ pdfdocuments = []
+ temporary_files = []
+
+ # In most case, the report is created for a single report and an attachment is created (business report).
+ # To avoid reading the resulting report two times (one to return the content to the user and one during
+ # the post process). Then, in this specific case, the content is kept in memory to avoid a second reading.
+ content_read = None
+
+ for wkhtmltopdf_obj in wkhtmltopdf_objs:
+ local_command_args = []
+ pdfreport_fd, pdfreport_path = tempfile.mkstemp(suffix='.pdf', prefix='report.tmp.')
+ temporary_files.append(pdfreport_path)
+
+ # Directly load the document if we already have it
+ if wkhtmltopdf_obj.attachment_id:
+ with closing(os.fdopen(pdfreport_fd, 'w')) as pdfreport:
+ pdfreport.write(base64.decodestring(wkhtmltopdf_obj.attachment_id.datas))
+ pdfdocuments.append(pdfreport_path)
+ continue
+ else:
+ os.close(pdfreport_fd)
+
+ # Wkhtmltopdf handles header/footer as separate pages. Create them if necessary.
+ if wkhtmltopdf_obj.header:
+ head_file_fd, head_file_path = tempfile.mkstemp(suffix='.html', prefix='report.header.tmp.')
+ temporary_files.append(head_file_path)
+ with closing(os.fdopen(head_file_fd, 'w')) as head_file:
+ head_file.write(wkhtmltopdf_obj.header)
+ local_command_args.extend(['--header-html', head_file_path])
+ if wkhtmltopdf_obj.footer:
+ foot_file_fd, foot_file_path = tempfile.mkstemp(suffix='.html', prefix='report.footer.tmp.')
+ temporary_files.append(foot_file_path)
+ with closing(os.fdopen(foot_file_fd, 'w')) as foot_file:
+ foot_file.write(wkhtmltopdf_obj.footer)
+ local_command_args.extend(['--footer-html', foot_file_path])
+
+ # Body stuff
+ content_file_fd, content_file_path = tempfile.mkstemp(suffix='.html', prefix='report.body.tmp.')
+ temporary_files.append(content_file_path)
+ with closing(os.fdopen(content_file_fd, 'w')) as content_file:
+ content_file.write(wkhtmltopdf_obj.content)
+
+ try:
+ wkhtmltopdf = [_get_wkhtmltopdf_bin()] + command_args + local_command_args
+ wkhtmltopdf += [content_file_path] + [pdfreport_path]
+ process = subprocess.Popen(wkhtmltopdf, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ out, err = process.communicate()
+
+ if process.returncode not in [0, 1]:
+ if process.returncode == -11:
+ message = _(
+ 'Wkhtmltopdf failed (error code: %s). Memory limit too low or maximum file number of subprocess reached. Message : %s')
+ else:
+ message = _('Wkhtmltopdf failed (error code: %s). Message: %s')
+ raise UserError(message % (str(process.returncode), err[-1000:]))
+ pdfdocuments.append(pdfreport_path)
+ except:
+ raise
+
+ # Call the postprocess method on the ir.actions.report.
+ if wkhtmltopdf_obj.res_id and wkhtmltopdf_obj.attachment_name:
+ with open(pdfreport_path, 'rb') as pdfreport:
+ content_read = pdfreport.read()
+ self.postprocess_pdf_report(wkhtmltopdf_obj.res_id, content_read, wkhtmltopdf_obj.attachment_name)
+
+ # Return the entire document
+ if len(pdfdocuments) == 1:
+ entire_report_path = pdfdocuments[0]
+ else:
+ entire_report_path = _merge_pdf(pdfdocuments)
+ temporary_files.append(entire_report_path)
+ content_read = None
+
+ if not content_read:
+ with open(entire_report_path, 'rb') as pdfdocument:
+ content_read = pdfdocument.read()
+
+ # Manual cleanup of the temporary files
+ for temporary_file in temporary_files:
+ try:
+ os.unlink(temporary_file)
+ except (OSError, IOError):
+ _logger.error('Error when trying to remove file %s' % temporary_file)
+
+ return content_read
+
+ @api.model
+ def _get_report_from_name(self, report_name):
+ """Get the first record of ir.actions.report having the ``report_name`` as value for
+ the field report_name.
+ """
+ report_obj = self.env['ir.actions.report']
+ qwebtypes = ['qweb-pdf', 'qweb-html']
+ conditions = [('report_type', 'in', qwebtypes), ('report_name', '=', report_name)]
+ context = self.env['res.users'].context_get()
+ return report_obj.with_context(context).search(conditions, limit=1)
+
+ def barcode(self, barcode_type, value, width=600, height=100, humanreadable=0):
+ if barcode_type == 'UPCA' and len(value) in (11, 12, 13):
+ barcode_type = 'EAN13'
+ if len(value) in (11, 12):
+ value = '0%s' % value
+ try:
+ width, height, humanreadable = int(width), int(height), bool(int(humanreadable))
+ barcode = createBarcodeDrawing(
+ barcode_type, value=value, format='png', width=width, height=height,
+ humanReadable=humanreadable
+ )
+ return barcode.asString('png')
+ except (ValueError, AttributeError):
+ raise ValueError("Cannot convert into barcode.")
+
+ @api.model
+ def render_template(self, template, values=None):
+ """Allow to render a QWeb template python-side. This function returns the 'ir.ui.view'
+ render but embellish it with some variables/methods used in reports.
+ :param values: additionnal methods/variables used in the rendering
+ :returns: html representation of the template
+ """
+ if values is None:
+ values = {}
+
+ context = dict(self.env.context, inherit_branding=True) # Tell QWeb to brand the generated html
+
+ # Browse the user instead of using the sudo self.env.user
+ user = self.env['res.users'].browse(self.env.uid)
+ website = None
+ if request and hasattr(request, 'website'):
+ if request.website is not None:
+ website = request.website
+ context = dict(context, translatable=context.get('lang') != request.website.default_lang_code)
+
+ view_obj = self.env['ir.ui.view'].with_context(context)
+ values.update(
+ time=time,
+ context_timestamp=lambda t: fields.Datetime.context_timestamp(self.with_context(tz=user.tz), t),
+ editable=True,
+ user=user,
+ res_company=user.company_id,
+ website=website,
+ web_base_url=self.env['ir.config_parameter'].sudo().get_param('web.base.url', default='')
+ )
+ return view_obj.render_template(template, values)
+
+ @api.model
+ def render_qweb_pdf(self, res_ids, html=None, data=None):
+ # In case of test environment without enough workers to perform calls to wkhtmltopdf,
+ # fallback to render_html.
+ if tools.config['test_enable'] and not tools.config['test_report_directory']:
+ return self.render_qweb_html(res_ids, data=data)
+
+ if self.get_wkhtmltopdf_state() == 'install':
+ # wkhtmltopdf is not installed
+ # the call should be catched before (cf /report/check_wkhtmltopdf) but
+ # if get_pdf is called manually (email template), the check could be
+ # bypassed
+ raise UserError(_("Unable to find Wkhtmltopdf on this system. The PDF can not be created."))
+
+ # As the assets are generated during the same transaction as the rendering of the
+ # templates calling them, there is a scenario where the assets are unreachable: when
+ # you make a request to read the assets while the transaction creating them is not done.
+ # Indeed, when you make an asset request, the controller has to read the `ir.attachment`
+ # table.
+ # This scenario happens when you want to print a PDF report for the first time, as the
+ # assets are not in cache and must be generated. To workaround this issue, we manually
+ # commit the writes in the `ir.attachment` table. It is done thanks to a key in the context.
+ context = dict(self.env.context)
+ if not config['test_enable']:
+ context['commit_assetsbundle'] = True
+
+ # Disable the debug mode in the PDF rendering in order to not split the assets bundle
+ # into separated files to load. This is done because of an issue in wkhtmltopdf
+ # failing to load the CSS/Javascript resources in time.
+ # Without this, the header/footer of the reports randomly disapear
+ # because the resources files are not loaded in time.
+ # https://github.com/wkhtmltopdf/wkhtmltopdf/issues/2083
+ context['debug'] = False
+
+ if html is None:
+ html = self.with_context(context).render_qweb_html(res_ids, data=data)[0]
+
+ # The test cursor prevents the use of another environnment while the current
+ # transaction is not finished, leading to a deadlock when the report requests
+ # an asset bundle during the execution of test scenarios. In this case, return
+ # the html version.
+ if isinstance(self.env.cr, TestCursor):
+ return html
+
+ html = html.decode('utf-8') # Ensure the current document is utf-8 encoded.
+
+ # Get the paperformat associated to the report, otherwise fallback on the company one.
+ if not self.paperformat_id:
+ user = self.env['res.users'].browse(self.env.uid) # Rebrowse to avoid sudo user from self.env.user
+ paperformat = user.company_id.paperformat_id
+ else:
+ paperformat = self.paperformat_id
+
+ html_data = self.with_context(context)._extract_wkhtmltopdf_data_from_html(res_ids, html)
+ wkhtmltopdf_objs = html_data.get('wkhtmltopdf_objs', [])
+ specific_paperformat_args = html_data.get('specific_paperformat_args', None)
+
+ return self._run_wkhtmltopdf(
+ wkhtmltopdf_objs,
+ context.get('landscape'),
+ paperformat,
+ specific_paperformat_args=specific_paperformat_args,
+ set_viewport_size=context.get('set_viewport_size'),
+ ), 'pdf'
+
+ @api.model
+ def render_qweb_html(self, docids, data=None):
+ """This method generates and returns html version of a report.
+ """
+ # If the report is using a custom model to render its html, we must use it.
+ # Otherwise, fallback on the generic html rendering.
+ report_model_name = 'report.%s' % self.report_name
+ report_model = self.env.get(report_model_name)
+
+ if report_model is not None:
+ data = report_model.get_report_values(docids, data=data)
+ else:
+ docs = self.env[self.model].browse(docids)
+ data = {
+ 'doc_ids': docids,
+ 'doc_model': self.model,
+ 'docs': docs,
+ }
+ return self.render_template(self.report_name, data), 'html'
+
+ @api.multi
+ def render(self, res_ids, data=None):
+ report_type = self.report_type.lower().replace('-', '_')
+ render_func = getattr(self, 'render_' + report_type, None)
+ if not render_func:
+ return None
+ return render_func(res_ids, data=data)
+
+ @api.noguess
+ def report_action(self, docids, data=None, config=True):
+ """Return an action of type ir.actions.report.
+
+ :param docids: id/ids/browserecord of the records to print (if not used, pass an empty list)
+ :param report_name: Name of the template to generate an action for
+ """
+ if (self.env.uid == SUPERUSER_ID) and ((not self.env.user.company_id.external_report_layout) or (not self.env.user.company_id.logo)) and config:
+ template = self.env.ref('report.view_company_report_form', False)
+ return {
+ 'name': _('Choose Your Document Layout'),
+ 'type': 'ir.actions.act_window',
+ 'context': {'default_report_name': self.report_name},
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_id': self.env.user.company_id.id,
+ 'res_model': 'res.company',
+ 'views': [(template.id, 'form')],
+ 'view_id': template.id,
+ 'target': 'new',
+ }
+
+ context = self.env.context
+ if docids:
+ if isinstance(docids, models.Model):
+ active_ids = docids.ids
+ elif isinstance(docids, int):
+ active_ids = [docids]
+ elif isinstance(docids, list):
+ active_ids = docids
+ context = dict(self.env.context, active_ids=active_ids)
+
+ return {
+ 'context': context,
+ 'data': data,
+ 'type': 'ir.actions.report',
+ 'report_name': self.report_name,
+ 'report_type': self.report_type,
+ 'report_file': self.report_file,
+ 'name': self.name,
+ }
diff --git a/odoo/addons/base/ir/ir_model_report.xml b/odoo/addons/base/ir/ir_model_report.xml
index 9cd79a16dfd..4baa1daf989 100644
--- a/odoo/addons/base/ir/ir_model_report.xml
+++ b/odoo/addons/base/ir/ir_model_report.xml
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
-
<report
id="report_ir_model_overview"
string="Model Overview"
diff --git a/odoo/addons/base/ir/ir_qweb/fields.py b/odoo/addons/base/ir/ir_qweb/fields.py
index a1f9014b6d8..75301c6c823 100644
--- a/odoo/addons/base/ir/ir_qweb/fields.py
+++ b/odoo/addons/base/ir/ir_qweb/fields.py
@@ -412,6 +412,28 @@ class RelativeDatetimeConverter(models.AbstractModel):
return super(RelativeDatetimeConverter, self).record_to_html(record, field_name, options)
+class BarcodeConverter(models.AbstractModel):
+ """ ``barcode`` widget rendering, inserts a data:uri-using image tag in the
+ document. May be overridden by e.g. the website module to generate links
+ instead.
+ """
+ _name = 'ir.qweb.field.barcode'
+ _inherit = 'ir.qweb.field'
+
+ @api.model
+ def value_to_html(self, value, options=None):
+ barcode_type = options.get('type', 'Code128')
+ barcode = self.env['ir.actions.report'].barcode(
+ barcode_type,
+ value,
+ **dict((key, value) for key, value in options.items() if key in ['width', 'height', 'humanreadable']))
+ return unicodifier('<img src="data:%s;base64,%s">' % ('png', barcode.encode('base64')))
+
+ @api.model
+ def from_html(self, model, field, element):
+ return None
+
+
class Contact(models.AbstractModel):
_name = 'ir.qweb.field.contact'
_inherit = 'ir.qweb.field.many2one'
diff --git a/odoo/addons/base/ir/report_ir_model.xml b/odoo/addons/base/ir/report_ir_model.xml
index 284767a3247..b7617a74b17 100644
--- a/odoo/addons/base/ir/report_ir_model.xml
+++ b/odoo/addons/base/ir/report_ir_model.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="report_irmodeloverview">
- <t t-call="report.html_container">
+ <t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<div class="page">
<table class="table table-bordered mb64">
diff --git a/odoo/addons/base/module/module_report.xml b/odoo/addons/base/module/module_report.xml
index bdf25887f36..bf1acbcd7d1 100644
--- a/odoo/addons/base/module/module_report.xml
+++ b/odoo/addons/base/module/module_report.xml
@@ -7,7 +7,6 @@
string="Technical guide"
model="ir.module.module"
report_type="qweb-pdf"
- auto="False"
file="base.report_irmodulereference"
name="base.report_irmodulereference"/>
diff --git a/odoo/addons/base/module/report/ir_module_reference_print.py b/odoo/addons/base/module/report/ir_module_reference_print.py
index d338f1d9913..1f2dff53243 100644
--- a/odoo/addons/base/module/report/ir_module_reference_print.py
+++ b/odoo/addons/base/module/report/ir_module_reference_print.py
@@ -25,15 +25,13 @@ class IrModelReferenceReport(models.AbstractModel):
return []
@api.model
- def render_html(self, docids, data=None):
- Report = self.env['report']
- report = Report._get_report_from_name('base.report_irmodulereference')
+ def get_report_values(self, docids, data=None):
+ report = self.env['ir.actions.report']._get_report_from_name('base.report_irmodulereference')
selected_modules = self.env['ir.module.module'].browse(docids)
- docargs = {
+ return {
'doc_ids': docids,
'doc_model': report.model,
'docs': selected_modules,
'findobj': self._object_find,
'findfields': self._fields_find,
}
- return Report.render('base.report_irmodulereference', docargs)
diff --git a/odoo/addons/base/module/report/report_ir_module_reference.xml b/odoo/addons/base/module/report/report_ir_module_reference.xml
index 45b7737df01..2fab9748cce 100644
--- a/odoo/addons/base/module/report/report_ir_module_reference.xml
+++ b/odoo/addons/base/module/report/report_ir_module_reference.xml
@@ -2,9 +2,9 @@
<odoo>
<data>
<template id="report_irmodulereference">
- <t t-call="report.html_container">
+ <t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
- <t t-call="report.internal_layout">
+ <t t-call="web.internal_layout">
<div class="page">
<h3 class="text-center">Introspection report on objects</h3>
<table class="table table-bordered mt32">
diff --git a/odoo/addons/base/res/__init__.py b/odoo/addons/base/res/__init__.py
index 1e10e5ee9cd..1fdc67e249f 100644
--- a/odoo/addons/base/res/__init__.py
+++ b/odoo/addons/base/res/__init__.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from . import report_paperformat
from . import res_country
from . import res_lang
from . import res_partner
diff --git a/odoo/addons/base/res/report_paperformat.py b/odoo/addons/base/res/report_paperformat.py
new file mode 100644
index 00000000000..08235f3060b
--- /dev/null
+++ b/odoo/addons/base/res/report_paperformat.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models, _
+from odoo.exceptions import ValidationError
+
+class report_paperformat(models.Model):
+ _name = "report.paperformat"
+ _description = "Allows customization of a report."
+
+ name = fields.Char('Name', required=True)
+ default = fields.Boolean('Default paper format ?')
+ format = fields.Selection([
+ ('A0', 'A0 5 841 x 1189 mm'),
+ ('A1', 'A1 6 594 x 841 mm'),
+ ('A2', 'A2 7 420 x 594 mm'),
+ ('A3', 'A3 8 297 x 420 mm'),
+ ('A4', 'A4 0 210 x 297 mm, 8.26 x 11.69 inches'),
+ ('A5', 'A5 9 148 x 210 mm'),
+ ('A6', 'A6 10 105 x 148 mm'),
+ ('A7', 'A7 11 74 x 105 mm'),
+ ('A8', 'A8 12 52 x 74 mm'),
+ ('A9', 'A9 13 37 x 52 mm'),
+ ('B0', 'B0 14 1000 x 1414 mm'),
+ ('B1', 'B1 15 707 x 1000 mm'),
+ ('B2', 'B2 17 500 x 707 mm'),
+ ('B3', 'B3 18 353 x 500 mm'),
+ ('B4', 'B4 19 250 x 353 mm'),
+ ('B5', 'B5 1 176 x 250 mm, 6.93 x 9.84 inches'),
+ ('B6', 'B6 20 125 x 176 mm'),
+ ('B7', 'B7 21 88 x 125 mm'),
+ ('B8', 'B8 22 62 x 88 mm'),
+ ('B9', 'B9 23 33 x 62 mm'),
+ ('B10', ':B10 16 31 x 44 mm'),
+ ('C5E', 'C5E 24 163 x 229 mm'),
+ ('Comm10E', 'Comm10E 25 105 x 241 mm, U.S. '
+ 'Common 10 Envelope'),
+ ('DLE', 'DLE 26 110 x 220 mm'),
+ ('Executive', 'Executive 4 7.5 x 10 inches, '
+ '190.5 x 254 mm'),
+ ('Folio', 'Folio 27 210 x 330 mm'),
+ ('Ledger', 'Ledger 28 431.8 x 279.4 mm'),
+ ('Legal', 'Legal 3 8.5 x 14 inches, '
+ '215.9 x 355.6 mm'),
+ ('Letter', 'Letter 2 8.5 x 11 inches, '
+ '215.9 x 279.4 mm'),
+ ('Tabloid', 'Tabloid 29 279.4 x 431.8 mm'),
+ ('custom', 'Custom')
+ ], 'Paper size', default='A4', help="Select Proper Paper size")
+ margin_top = fields.Float('Top Margin (mm)', default=40)
+ margin_bottom = fields.Float('Bottom Margin (mm)', default=20)
+ margin_left = fields.Float('Left Margin (mm)', default=7)
+ margin_right = fields.Float('Right Margin (mm)', default=7)
+ page_height = fields.Integer('Page height (mm)', default=False)
+ page_width = fields.Integer('Page width (mm)', default=False)
+ orientation = fields.Selection([
+ ('Landscape', 'Landscape'),
+ ('Portrait', 'Portrait')
+ ], 'Orientation', default='Landscape')
+ header_line = fields.Boolean('Display a header line', default=False)
+ header_spacing = fields.Integer('Header spacing', default=35)
+ dpi = fields.Integer('Output DPI', required=True, default=90)
+ report_ids = fields.One2many('ir.actions.report', 'paperformat_id', 'Associated reports', help="Explicitly associated reports")
+
+ @api.constrains('format')
+ def _check_format_or_page(self):
+ if self.filtered(lambda x: x.format != 'custom' and (x.page_width or x.page_height)):
+ raise ValidationError(_('Error ! You cannot select a format AND specific page width/height.'))
diff --git a/odoo/addons/base/res/report_paperformat_views.xml b/odoo/addons/base/res/report_paperformat_views.xml
new file mode 100644
index 00000000000..2339d00e97f
--- /dev/null
+++ b/odoo/addons/base/res/report_paperformat_views.xml
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+ <!-- report.paperformat -->
+ <record id="paperformat_view_tree" model="ir.ui.view">
+ <field name="name">paper_format_view_tree</field>
+ <field name="model">report.paperformat</field>
+ <field name="arch" type="xml">
+ <tree string="Paper format configuration">
+ <field name="name" />
+ </tree>
+ </field>
+ </record>
+ <record id="paperformat_view_form" model="ir.ui.view">
+ <field name="name">paper_format_view_form</field>
+ <field name="model">report.paperformat</field>
+ <field name="arch" type="xml">
+ <form string="Paper format configuration">
+ <group>
+ <field name="name" />
+ <field name="format" />
+ <field name="page_height" attrs="{'invisible': [('format','!=','custom')]}" />
+ <field name="page_width" attrs="{'invisible': [('format','!=','custom')]}" />
+ <field name="orientation" />
+ <field name="margin_top" />
+ <field name="margin_bottom" />
+ <field name="margin_left" />
+ <field name="margin_right" />
+ <field name="header_line" />
+ <field name="header_spacing" />
+ <field name="dpi" />
+ <field name="report_ids" widget="many2many_tags"/>
+ </group>
+ </form>
+ </field>
+ </record>
+ <record id="paper_format_action" model="ir.actions.act_window">
+ <field name="name">Paper Format General Configuration</field>
+ <field name="res_model">report.paperformat</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+ <record id='reports_action' model='ir.actions.act_window'>
+ <field name="name">Reports</field>
+ <field name="res_model">ir.actions.report</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+ <menuitem
+ id="reporting_menuitem"
+ name="Reporting"
+ parent="base.menu_custom"
+ sequence="15"
+ groups="base.group_no_one"
+ />
+ <menuitem
+ id="paper_format_menuitem"
+ name="Paper Format"
+ parent="reporting_menuitem"
+ action="paper_format_action"
+ sequence="2"
+ groups="base.group_no_one"
+ />
+ <menuitem
+ id="reports_menuitem"
+ name="Reporting"
+ parent="reporting_menuitem"
+ action="reports_action"
+ sequence="3"
+ groups="base.group_no_one"
+ />
+
+ <!-- Customize the company form view by adding paperformat field as well as demo reports -->
+ <!-- Demo reports definition -->
+ <template id="preview_internalreport">
+ <t t-call="web.html_container">
+ <t t-call="web.internal_layout">
+ <div class="page">
+ <p>This is a sample of an internal report.</p>
+ </div>
+ </t>
+ </t>
+ </template>
+ <template id="preview_externalreport">
+ <t t-call="web.html_container">
+ <t t-call="web.external_layout">
+ <div class="page">
+ <p>This is a sample of an external report.</p>
+ </div>
+ </t>
+ </t>
+ </template>
+ <report id="action_report_internalpreview"
+ model="res.company"
+ name="base.preview_internalreport"
+ file="base.preview_internalreport"
+ string="Preview Internal Report"
+ report_type="qweb-pdf"/>
+ <report id="action_report_externalpreview"
+ model="res.company"
+ name="base.preview_externalreport"
+ file="base.preview_externalreport"
+ string="Preview External Report"
+ report_type="qweb-pdf"/>
+ </data>
+</odoo>
diff --git a/odoo/addons/base/res/res_company.py b/odoo/addons/base/res/res_company.py
index b02255db4bd..96e922f5e77 100644
--- a/odoo/addons/base/res/res_company.py
+++ b/odoo/addons/base/res/res_company.py
@@ -52,11 +52,28 @@ class Company(models.Model):
vat = fields.Char(related='partner_id.vat', string="TIN")
company_registry = fields.Char()
sequence = fields.Integer(help='Used to order Companies in the company switcher', default=10)
+ paperformat_id = fields.Many2one('report.paperformat', 'Paper format', default=lambda self: self.env.ref('report.paperformat_euro', raise_if_not_found=False))
+ external_report_layout = fields.Selection([
+ ('background', 'Background'),
+ ('boxed', 'Boxed'),
+ ('clean', 'Clean'),
+ ('standard', 'Standard'),
+ ], string='Document Template')
_sql_constraints = [
('name_uniq', 'unique (name)', 'The company name must be unique !')
]
+ @api.model_cr
+ def init(self):
+ for company in self.search([('paperformat_id', '=', False)]):
+ paperformat_euro = self.env.ref('report.paperformat_euro', False)
+ if paperformat_euro:
+ company.write({'paperformat_id': paperformat_euro.id})
+ sup = super(Company, self)
+ if hasattr(sup, 'init'):
+ sup.init()
+
def _get_company_address_fields(self, partner):
return {
'street' : partner.street,
@@ -215,3 +232,20 @@ class Company(models.Model):
def _check_parent_id(self):
if not self._check_recursion():
raise ValidationError(_('Error ! You cannot create recursive companies.'))
+
+ @api.multi
+ def open_company_edit_report(self):
+ self.ensure_one()
+ return self.env['base.config.settings'].open_company()
+
+ @api.multi
+ def set_report_template(self):
+ self.ensure_one()
+ if self.env.context.get('report_template', False):
+ self.external_report_layout = self.env.context['report_template']
+ if self.env.context.get('default_report_name'):
+ document = self.env.get(self.env.context['active_model']).browse(self.env.context['active_id'])
+ report_name = self.env.context['default_report_name']
+ report_action = self.env['ir.actions.report'].search([('report_name', '=', report_name)], limit=1)
+ return report_action.report_action(document, config=False)
+ return False
diff --git a/odoo/addons/base/res/res_company_view.xml b/odoo/addons/base/res/res_company_view.xml
index 3a7168dc3f8..a583ce9f5e5 100644
--- a/odoo/addons/base/res/res_company_view.xml
+++ b/odoo/addons/base/res/res_company_view.xml
@@ -85,5 +85,51 @@
<field eval="'ir.actions.act_window,%d'%company_normal_action_tree" name="value"/>
</record>
+ <record id="view_company_report_form" model="ir.ui.view">
+ <field name="name">res.company.report.form</field>
+ <field name="model">res.company</field>
+ <field name="priority">1024</field>
+ <field name="arch" type="xml">
+ <form string="Company">
+ <field name="logo" invisible="1"/>
+ <div class="alert alert-warning" attrs="{'invisible': [('logo','!=',False)]}">
+ <p>
+ <strong>No logo found!</strong>
+ To get a beautiful document, you should review your
+ company data: upload a logo, set
+ your address, etc.
+ </p>
+ <button name="open_company_edit_report" type="object" string="⇒ Review Company Data" class="btn-link"/>
+ </div>
+ <div class="row mt16">
+ <div class="col-xs-3 text-center">
+ <button type="object" name="set_report_template" context="{'report_template': 'background'}">
+ <img src="/report/static/src/img/preview_background.png" class="img img-responsive img-thumbnail" alt="Preview Background Template"/>
+ </button>
+ <a href="/report/static/src/pdf/preview_background.pdf" target="_blank">preview</a>
+ </div><div class="col-xs-3 text-center">
+ <button type="object" name="set_report_template" context="{'report_template': 'boxed'}">
+ <img src="/report/static/src/img/preview_boxed.png" class="img img-responsive img-thumbnail" alt="Preview Boxed Template"/>
+ </button>
+ <a href="/report/static/src/pdf/preview_boxed.pdf" target="_blank">preview</a>
+ </div><div class="col-xs-3 text-center">
+ <button type="object" name="set_report_template" context="{'report_template': 'clean'}">
+ <img src="/report/static/src/img/preview_clean.png" class="img img-responsive img-thumbnail" alt="Preview Clean Template"/>
+ </button>
+ <a href="/report/static/src/pdf/preview_clean.pdf" target="_blank">preview</a>
+ </div><div class="col-xs-3 text-center">
+ <button type="object" name="set_report_template" context="{'report_template': 'standard'}">
+ <img src="/report/static/src/img/preview_standard.png" class="img img-responsive img-thumbnail" alt="Preview Minimalist Template"/>
+ </button>
+ <a href="/report/static/src/pdf/preview_standard.pdf" target="_blank">preview</a>
+ </div>
+ </div>
+ <footer>
+ <button string="Close" class="btn-default" special="cancel" />
+ </footer>
+ </form>
+ </field>
+ </record>
+
</data>
</odoo>
diff --git a/odoo/addons/base/security/ir.model.access.csv b/odoo/addons/base/security/ir.model.access.csv
index 50a0d8ec762..842b6bb5fcc 100644
--- a/odoo/addons/base/security/ir.model.access.csv
+++ b/odoo/addons/base/security/ir.model.access.csv
@@ -92,3 +92,5 @@
"access_ir_mail_server","ir_mail_server","model_ir_mail_server","group_system",1,1,1,1
"access_ir_actions_client","ir_actions_client all","model_ir_actions_client",,1,0,0,0
"access_ir_logging","ir_logging admin","model_ir_logging","group_erp_manager",1,1,1,1
+"paperformat_access_portal","ir_actions_report_paperformat group_portal","model_report_paperformat",,1,0,0,0
+"paperformat_access_employee","ir_actions_report_paperformat group_hr_user","model_report_paperformat",,1,0,1,0
diff --git a/odoo/addons/base/static/img/bg_background_template.jpg b/odoo/addons/base/static/img/bg_background_template.jpg
new file mode 100644
index 00000000000..be776b47c8f
--- /dev/null
+++ b/odoo/addons/base/static/img/bg_background_template.jpg
Binary files differ
diff --git a/odoo/addons/base/static/img/logo_sample.png b/odoo/addons/base/static/img/logo_sample.png
new file mode 100644
index 00000000000..49090033fa3
--- /dev/null
+++ b/odoo/addons/base/static/img/logo_sample.png
Binary files differ
diff --git a/odoo/addons/base/static/img/preview_background.png b/odoo/addons/base/static/img/preview_background.png
new file mode 100644
index 00000000000..6ab7ee46456
--- /dev/null
+++ b/odoo/addons/base/static/img/preview_background.png
Binary files differ
diff --git a/odoo/addons/base/static/img/preview_boxed.png b/odoo/addons/base/static/img/preview_boxed.png
new file mode 100644
index 00000000000..758dc4640cc
--- /dev/null
+++ b/odoo/addons/base/static/img/preview_boxed.png
Binary files differ
diff --git a/odoo/addons/base/static/img/preview_clean.png b/odoo/addons/base/static/img/preview_clean.png
new file mode 100644
index 00000000000..439082ceb37
--- /dev/null
+++ b/odoo/addons/base/static/img/preview_clean.png
Binary files differ
diff --git a/odoo/addons/base/static/img/preview_standard.png b/odoo/addons/base/static/img/preview_standard.png
new file mode 100644
index 00000000000..ef23a5300cc
--- /dev/null
+++ b/odoo/addons/base/static/img/preview_standard.png
Binary files differ
diff --git a/odoo/addons/base/static/pdf/preview_background.pdf b/odoo/addons/base/static/pdf/preview_background.pdf
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/odoo/addons/base/static/pdf/preview_background.pdf
diff --git a/odoo/addons/base/static/pdf/preview_boxed.pdf b/odoo/addons/base/static/pdf/preview_boxed.pdf
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/odoo/addons/base/static/pdf/preview_boxed.pdf
diff --git a/odoo/addons/base/static/pdf/preview_clean.pdf b/odoo/addons/base/static/pdf/preview_clean.pdf
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/odoo/addons/base/static/pdf/preview_clean.pdf
diff --git a/odoo/addons/base/static/pdf/preview_standard.pdf b/odoo/addons/base/static/pdf/preview_standard.pdf
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/odoo/addons/base/static/pdf/preview_standard.pdf
diff --git a/odoo/addons/base/tests/__init__.py b/odoo/addons/base/tests/__init__.py
index 6ae162486d6..1b6fe723b2b 100644
--- a/odoo/addons/base/tests/__init__.py
+++ b/odoo/addons/base/tests/__init__.py
@@ -32,3 +32,4 @@ from . import test_view_validation
from . import test_views
from . import test_xmlrpc
from . import test_res_partner_bank
+from . import test_reports
diff --git a/odoo/addons/base/tests/test_reports.py b/odoo/addons/base/tests/test_reports.py
new file mode 100644
index 00000000000..85e5960b03e
--- /dev/null
+++ b/odoo/addons/base/tests/test_reports.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import logging
+
+import odoo
+import odoo.tests
+
+
+_logger = logging.getLogger(__name__)
+
+
+@odoo.tests.common.at_install(False)
+@odoo.tests.common.post_install(True)
+class TestReports(odoo.tests.TransactionCase):
+ def test_reports(self):
+ domain = [('report_type', 'like', 'qweb')]
+ for report in self.env['ir.actions.report'].search(domain):
+ report_model = 'report.%s' % report.report_name
+ try:
+ self.env[report_model]
+ except KeyError:
+ # Only test the generic reports here
+ _logger.info("testing report %s", report.report_name)
+ report_model = self.env[report.model]
+ report_records = report_model.search([], limit=10)
+ if not report_records:
+ _logger.info("no record found skipping report %s", report.report_name)
+ if not report.multi:
+ report_records = report_records[:1]
+
+ # Test report generation
+ report.render_qweb_html(report_records.ids)
+ else:
+ continue
diff --git a/odoo/tools/yaml_import.py b/odoo/tools/yaml_import.py
index 58e9a154a54..36c6c248249 100644
--- a/odoo/tools/yaml_import.py
+++ b/odoo/tools/yaml_import.py
@@ -796,10 +796,6 @@ class YamlInterpreter(object):
for field,dest in (('file', 'report_file'), ('attachment','attachment'),('attachment_use','attachment_use')):
if getattr(node, field):
values[dest] = getattr(node, field)
- if node.auto:
- values['auto'] = safe_eval(node.auto)
- if node.header:
- values['header'] = safe_eval(node.header)
values['multi'] = node.multi and safe_eval(node.multi)
xml_id = node.id
self.validate_xml_id(xml_id)