Browse Source

[ADD] Header and Footer configuration for XLSX report

pull/320/head
Arnaud Pineux 5 years ago
committed by Stéphane Bidoul (ACSONE)
parent
commit
f139d0fca4
No known key found for this signature in database GPG Key ID: BCAB2555446B5B92
  1. 10
      report_xlsx/README.rst
  2. 6
      report_xlsx/__manifest__.py
  3. 1
      report_xlsx/models/__init__.py
  4. 85
      report_xlsx/models/header_footer.py
  5. 6
      report_xlsx/models/ir_report.py
  6. 15
      report_xlsx/report/report_xlsx.py
  7. 3
      report_xlsx/security/ir.model.access.csv
  8. 5
      report_xlsx/tests/__init__.py
  9. 102
      report_xlsx/tests/test_header_footer.py
  10. 64
      report_xlsx/views/header_footer.xml
  11. 20
      report_xlsx/views/ir_report.xml

10
report_xlsx/README.rst

@ -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
----------

6
report_xlsx/__manifest__.py

@ -16,5 +16,11 @@
'depends': [
'base',
],
'data': [
'security/ir.model.access.csv',
'views/header_footer.xml',
'views/ir_report.xml',
],
'installable': True,
}

1
report_xlsx/models/__init__.py

@ -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

@ -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

6
report_xlsx/models/ir_report.py

@ -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')])

15
report_xlsx/report/report_xlsx.py

@ -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

@ -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

@ -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

@ -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

@ -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="&amp;LHello &amp;CWorld &amp;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;">&amp;G</span> in the value.</div>
<div>
If you add the three images, the value must contain <span style="background-color: #ebecec;">&amp;L&amp;G &amp;C&amp;G &amp;R&amp;G</span> or <span style="background-color: #ebecec;">&amp;L&amp;[Picture] &amp;C&amp;[Picture] &amp;R&amp;[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

@ -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>
Loading…
Cancel
Save