mirror of
https://github.com/OCA/reporting-engine.git
synced 2025-02-16 16:30:38 +02:00
[ADD] Header and Footer configuration for XLSX report
This commit is contained in:
committed by
Stéphane Bidoul (ACSONE)
parent
dc1e660c99
commit
f139d0fca4
@@ -53,6 +53,15 @@ A report XML record ::
|
||||
attachment_use="False"
|
||||
/>
|
||||
|
||||
**XLSX Header & Footer**
|
||||
|
||||
You can configure them on the menu *Settings > Technical > Reports > XLSX Header/Footer* following the syntax from
|
||||
`xlsxwriter documentation <https://xlsxwriter.readthedocs.io/page_setup.html#set_header>`_.
|
||||
|
||||
Example of Header / Footer syntax : ``&LPage &P of &N &CFilename: &F &RSheetname: &A``
|
||||
|
||||
On a report XML with ``report_type == 'xlsx'`` you can specified the Header and Footer you configured.
|
||||
|
||||
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
||||
:alt: Try me on Runbot
|
||||
:target: https://runbot.odoo-community.org/runbot/143/10.0
|
||||
@@ -72,6 +81,7 @@ Contributors
|
||||
------------
|
||||
|
||||
* Adrien Peiffer <adrien.peiffer@acsone.eu>
|
||||
* Arnaud Pineux <arnaud.pineux@acsone.eu>
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
@@ -16,5 +16,11 @@
|
||||
'depends': [
|
||||
'base',
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
|
||||
'views/header_footer.xml',
|
||||
'views/ir_report.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
# Copyright 2015 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).+
|
||||
|
||||
from . import header_footer
|
||||
from . import ir_report
|
||||
|
||||
85
report_xlsx/models/header_footer.py
Normal file
85
report_xlsx/models/header_footer.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
import io
|
||||
import ast
|
||||
from odoo import fields, models, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class ReportHeaderFooter(models.Model):
|
||||
_name = 'report.xlsx.hf'
|
||||
|
||||
name = fields.Char(string="Name", required=True)
|
||||
hf_type = fields.Selection(
|
||||
[('header', 'Header'), ('footer', 'Footer')],
|
||||
string="Type",
|
||||
required=True)
|
||||
value = fields.Char(string="Value")
|
||||
manual_options = fields.Char(string="Options")
|
||||
image_left = fields.Binary(string='Image left')
|
||||
image_left_name = fields.Char('File Name')
|
||||
image_center = fields.Binary(string='Image center')
|
||||
image_center_name = fields.Char('File Name')
|
||||
image_right = fields.Binary(string='Image right')
|
||||
image_right_name = fields.Char('File Name')
|
||||
header_report_ids = fields.One2many(
|
||||
'ir.actions.report.xml',
|
||||
'header_id',
|
||||
string="Associated report(s)")
|
||||
footer_report_ids = fields.One2many(
|
||||
'ir.actions.report.xml',
|
||||
'footer_id',
|
||||
string="Associated report(s)")
|
||||
|
||||
@api.multi
|
||||
@api.constrains('manual_options')
|
||||
def _check_manual_options(self):
|
||||
for rec in self:
|
||||
if rec.manual_options:
|
||||
options = ast.literal_eval(rec.manual_options)
|
||||
if not isinstance(options, dict):
|
||||
raise ValidationError(
|
||||
_('The Header/Footer is not configured properly.\
|
||||
Options must be a dictionary.'))
|
||||
|
||||
@api.multi
|
||||
@api.constrains('image_left', 'image_center', 'image_right')
|
||||
def _check_images(self):
|
||||
for rec in self:
|
||||
error = ""
|
||||
if rec.image_left and ("&L&G" not in rec.value
|
||||
and "&L&[Picture]" not in rec.value):
|
||||
error += _('You must specify the control character &L&G or \
|
||||
&L&[Picture] in the "Value" when you add an "Image left".\n')
|
||||
if rec.image_center and ("&C&G" not in rec.value
|
||||
and "&C&[Picture]" not in rec.value):
|
||||
error += _('You must specify the control character &C&G or \
|
||||
&C&[Picture] in the "Value" when you add an "Image center".\n')
|
||||
if rec.image_right and ("&R&G" not in rec.value
|
||||
and "&R&[Picture]" not in rec.value):
|
||||
error += _('You must specify the control character &R&G or \
|
||||
&R&[Picture] in the "Value" when you add an "Image right".\n')
|
||||
if error:
|
||||
raise ValidationError(error)
|
||||
|
||||
@api.multi
|
||||
def get_options(self):
|
||||
self.ensure_one()
|
||||
options = {}
|
||||
if self.manual_options:
|
||||
options = ast.literal_eval(self.manual_options)
|
||||
if self.image_left:
|
||||
options['image_left'] = self.image_left_name
|
||||
options['image_data_left'] = io.BytesIO(
|
||||
self.image_left.decode('base64'))
|
||||
if self.image_center:
|
||||
options['image_center'] = self.image_center_name
|
||||
options['image_data_center'] = io.BytesIO(
|
||||
self.image_center.decode('base64'))
|
||||
if self.image_right:
|
||||
options['image_right'] = self.image_right_name
|
||||
options['image_data_right'] = io.BytesIO(
|
||||
self.image_right.decode('base64'))
|
||||
return options
|
||||
@@ -9,3 +9,9 @@ class IrActionsReportXml(models.Model):
|
||||
_inherit = 'ir.actions.report.xml'
|
||||
|
||||
report_type = fields.Selection(selection_add=[("xlsx", "xlsx")])
|
||||
header_id = fields.Many2one('report.xlsx.hf',
|
||||
string="Header",
|
||||
domain=[('hf_type', '=', 'header')])
|
||||
footer_id = fields.Many2one('report.xlsx.hf',
|
||||
string="Footer",
|
||||
domain=[('hf_type', '=', 'footer')])
|
||||
|
||||
@@ -28,6 +28,18 @@ class ReportXlsx(report_sxw):
|
||||
return self.create_xlsx_report(ids, data, report)
|
||||
return super(ReportXlsx, self).create(cr, uid, ids, data, context)
|
||||
|
||||
def create_workbook(self, file_data, data, objs, report):
|
||||
workbook = xlsxwriter.Workbook(file_data, self.get_workbook_options())
|
||||
self.generate_xlsx_report(workbook, data, objs)
|
||||
for sheet in workbook.worksheets():
|
||||
if report and report.header_id and report.header_id.value:
|
||||
sheet.set_header(report.header_id.value,
|
||||
report.header_id.get_options())
|
||||
if report and report.footer_id and report.footer_id.value:
|
||||
sheet.set_footer(report.footer_id.value,
|
||||
report.footer_id.get_options())
|
||||
return workbook
|
||||
|
||||
def create_xlsx_report(self, ids, data, report):
|
||||
self.parser_instance = self.parser(
|
||||
self.env.cr, self.env.uid, self.name2, self.env.context)
|
||||
@@ -35,8 +47,7 @@ class ReportXlsx(report_sxw):
|
||||
self.env.cr, self.env.uid, ids, self.env.context)
|
||||
self.parser_instance.set_context(objs, data, ids, 'xlsx')
|
||||
file_data = StringIO()
|
||||
workbook = xlsxwriter.Workbook(file_data, self.get_workbook_options())
|
||||
self.generate_xlsx_report(workbook, data, objs)
|
||||
workbook = self.create_workbook(file_data, data, objs, report)
|
||||
workbook.close()
|
||||
file_data.seek(0)
|
||||
return (file_data.read(), 'xlsx')
|
||||
|
||||
3
report_xlsx/security/ir.model.access.csv
Normal file
3
report_xlsx/security/ir.model.access.csv
Normal file
@@ -0,0 +1,3 @@
|
||||
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
|
||||
"access_report_xlsx_hf_all","report_xlsx_hf","model_report_xlsx_hf",,1,0,0,0
|
||||
"access_report_xlsx_hf_group_system","report_xlsx_hf_group_system","model_report_xlsx_hf","base.group_system",1,1,1,1
|
||||
|
5
report_xlsx/tests/__init__.py
Normal file
5
report_xlsx/tests/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from . import test_header_footer
|
||||
102
report_xlsx/tests/test_header_footer.py
Normal file
102
report_xlsx/tests/test_header_footer.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
import odoo.tests.common as common
|
||||
from odoo.exceptions import ValidationError
|
||||
from cStringIO import StringIO
|
||||
from odoo.addons.report_xlsx.report.report_xlsx import ReportXlsx
|
||||
|
||||
|
||||
class PartnerXlsx(ReportXlsx):
|
||||
|
||||
def generate_xlsx_report(self, workbook, data, partners):
|
||||
sheet = workbook.add_worksheet('sheet')
|
||||
sheet.write(0, 0, 'test')
|
||||
|
||||
|
||||
class TestHeaderFooter(common.TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestHeaderFooter, cls).setUpClass()
|
||||
cls.report = PartnerXlsx('report.res.partner.xlsx',
|
||||
'res.partner')
|
||||
|
||||
def setUp(self):
|
||||
super(TestHeaderFooter, self).setUp()
|
||||
|
||||
# Create Header
|
||||
self.header_001 = self.env['report.xlsx.hf'].create({
|
||||
'name': 'Header 001',
|
||||
'hf_type': 'header',
|
||||
'value': '&LPage &P of &N &CFilename: &F &RSheetname: &A',
|
||||
})
|
||||
# Create Footer
|
||||
self.footer_001 = self.env['report.xlsx.hf'].create({
|
||||
'name': 'Footer 001',
|
||||
'hf_type': 'footer',
|
||||
'value': '&LCurrent date: &D &RCurrent time: &T',
|
||||
})
|
||||
# Create Report
|
||||
self.report_xlsx = self.env['ir.actions.report.xml'].create({
|
||||
'report_name': 'res.partner.xlsx',
|
||||
'name': 'XLSX report',
|
||||
'report_type': 'xlsx',
|
||||
'model': 'res.partner',
|
||||
'header_id': self.header_001.id,
|
||||
'footer_id': self.footer_001.id,
|
||||
})
|
||||
|
||||
def test_header_footer(self):
|
||||
"""
|
||||
Check that the header and footer have been added to the worksheets
|
||||
"""
|
||||
file_data = StringIO()
|
||||
partner = self.env['res.partner'].browse([1])
|
||||
workbook = self.report.create_workbook(
|
||||
file_data, {}, partner, self.report_xlsx)
|
||||
header = u'&LPage &P of &N &CFilename: &F &RSheetname: &A'
|
||||
footer = u'&LCurrent date: &D &RCurrent time: &T'
|
||||
|
||||
for sheet in workbook.worksheets():
|
||||
self.assertEqual(header, sheet.header)
|
||||
self.assertEqual(footer, sheet.footer)
|
||||
|
||||
def test_wrong_options(self):
|
||||
"""
|
||||
Check that options must be a dict
|
||||
"""
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env['report.xlsx.hf'].create({
|
||||
'name': 'Header ERROR',
|
||||
'hf_type': 'header',
|
||||
'value': '&LPage &P of &N &CFilename: &F &RSheetname: &A',
|
||||
'manual_options': "1234",
|
||||
})
|
||||
|
||||
def test_image_options(self):
|
||||
"""
|
||||
Check that, adding image, modify the options
|
||||
"""
|
||||
header = self.env['report.xlsx.hf'].create({
|
||||
'name': 'Header IMAGE',
|
||||
'hf_type': 'header',
|
||||
'value': '&L&G &C&G &R&G',
|
||||
'image_left':
|
||||
'R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==',
|
||||
'image_left_name': 'image_left.jpg',
|
||||
'image_center':
|
||||
'R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==',
|
||||
'image_center_name': 'image_center.jpg',
|
||||
'image_right':
|
||||
'R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==',
|
||||
'image_right_name': 'image_right.jpg',
|
||||
})
|
||||
options = header.get_options()
|
||||
self.assertEqual(options.get('image_left'), 'image_left.jpg')
|
||||
self.assertEqual(options.get('image_center'), 'image_center.jpg')
|
||||
self.assertEqual(options.get('image_right'), 'image_right.jpg')
|
||||
self.assertTrue(options.get('image_data_left'))
|
||||
self.assertTrue(options.get('image_data_center'))
|
||||
self.assertTrue(options.get('image_data_right'))
|
||||
64
report_xlsx/views/header_footer.xml
Normal file
64
report_xlsx/views/header_footer.xml
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="report_xlsx_hf_tree" model="ir.ui.view">
|
||||
<field name="name">report_xlsx_hf Tree</field>
|
||||
<field name="model">report.xlsx.hf</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="hf_type"/>
|
||||
<field name="value"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="report_xlsx_hf_form" model="ir.ui.view">
|
||||
<field name="name">report_xlsx_hf Form</field>
|
||||
<field name="model">report.xlsx.hf</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="hf_type"/>
|
||||
</group>
|
||||
<div colspan="2"><i class="fa fa-info-circle" aria-hidden="true"/> Search in the <a href="https://xlsxwriter.readthedocs.io/page_setup.html#set_header">documentation</a> for set_header and set_footer to find the proper syntax to fill "Value" and "Options".</div>
|
||||
<group>
|
||||
<field name="value" placeholder="&LHello &CWorld &R!!!"/>
|
||||
<field name="manual_options" placeholder="{'margin': 0.3}"/>
|
||||
</group>
|
||||
<separator string="Images"/>
|
||||
<div><i class="fa fa-info-circle" aria-hidden="true"/>
|
||||
If you select an image, you don't have to specify any image options (image_left, image_data_left, ...) but you need to add the control character <span style="background-color: #ebecec;">&G</span> in the value.</div>
|
||||
<div>
|
||||
If you add the three images, the value must contain <span style="background-color: #ebecec;">&L&G &C&G &R&G</span> or <span style="background-color: #ebecec;">&L&[Picture] &C&[Picture] &R&[Picture]</span>.</div>
|
||||
<group>
|
||||
<field name="image_left_name" invisible="1"/>
|
||||
<field name="image_left" filename="image_left_name"/>
|
||||
<field name="image_center_name" invisible="1"/>
|
||||
<field name="image_center" filename="image_center_name"/>
|
||||
<field name="image_right_name" invisible="1"/>
|
||||
<field name="image_right" filename="image_right_name"/>
|
||||
</group>
|
||||
<separator string="Associated report(s)"/>
|
||||
<field name="header_report_ids" attrs="{'invisible': [('hf_type', '!=', 'header')]}" nolabel="1" colspan="2"/>
|
||||
<field name="footer_report_ids" attrs="{'invisible': [('hf_type', '!=', 'footer')]}" nolabel="1" colspan="2"/>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="report_xlsx_hf_action" model="ir.actions.act_window">
|
||||
<field name="name">XLSX Header/Footer</field>
|
||||
<field name="res_model">report.xlsx.hf</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="report_xlsx_hf_menu" parent="report.reporting_menuitem"
|
||||
name="XLSX Header/Footer" action="report_xlsx_hf_action" sequence="100"/>
|
||||
|
||||
</odoo>
|
||||
20
report_xlsx/views/ir_report.xml
Normal file
20
report_xlsx/views/ir_report.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="act_report_xml_view_inherit" model="ir.ui.view">
|
||||
<field name="name">ir.actions.report.xml (xlsx header footer)</field>
|
||||
<field name="model">ir.actions.report.xml</field>
|
||||
<field name="inherit_id" ref="base.act_report_xml_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="Header/Footer" attrs="{'invisible': [('report_type', '!=', 'xlsx')]}">
|
||||
<group>
|
||||
<field name="header_id" context="{'default_hf_type': 'header'}"/>
|
||||
<field name="footer_id" context="{'default_hf_type': 'footer'}"/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user