From 781a74771475e9f9fcda7ad41b6eeb4a43c2fda6 Mon Sep 17 00:00:00 2001 From: Luc De Meyer Date: Mon, 24 Sep 2018 11:08:24 +0200 Subject: [PATCH] add modules --- account_move_line_report_xls/README.rst | 91 +++ account_move_line_report_xls/__init__.py | 8 + account_move_line_report_xls/__manifest__.py | 17 + account_move_line_report_xls/i18n/fr.po | 181 ++++++ account_move_line_report_xls/i18n/nl.po | 181 ++++++ .../models/__init__.py | 1 + .../models/account_move_line.py | 48 ++ .../report/__init__.py | 1 + .../report/account_move_line_xlsx.py | 444 ++++++++++++++ .../report/account_move_line_xlsx.xml | 15 + .../static/description/icon.png | Bin 0 -> 4723 bytes .../static/description/icon.svg | 320 ++++++++++ .../static/description/index.html | 108 ++++ .../static/description/journal_items.png | Bin 0 -> 46725 bytes .../static/description/oca_logo.png | Bin 0 -> 13383 bytes .../tests/__init__.py | 1 + .../tests/test_aml_report_xlsx.py | 36 ++ report_xlsx_helper/README.rst | 88 +++ report_xlsx_helper/__init__.py | 3 + report_xlsx_helper/__manifest__.py | 16 + report_xlsx_helper/controllers/__init__.py | 1 + report_xlsx_helper/controllers/main.py | 54 ++ .../i18n/report_xlsx_helper.pot | 67 +++ report_xlsx_helper/models/__init__.py | 1 + .../models/ir_actions_report.py | 21 + report_xlsx_helper/report/__init__.py | 2 + .../report/report_xlsx_abstract.py | 551 ++++++++++++++++++ .../report/test_partner_report_xlsx.py | 94 +++ .../static/description/icon.png | Bin 0 -> 9455 bytes report_xlsx_helper/tests/__init__.py | 1 + .../tests/test_report_xlsx_helper.py | 23 + report_xlsx_helper_demo/README.rst | 58 ++ report_xlsx_helper_demo/__init__.py | 2 + report_xlsx_helper_demo/__manifest__.py | 18 + report_xlsx_helper_demo/models/__init__.py | 1 + report_xlsx_helper_demo/models/res_partner.py | 25 + report_xlsx_helper_demo/report/__init__.py | 1 + .../report/partner_export_xlsx.py | 99 ++++ .../static/description/icon.png | Bin 0 -> 9455 bytes report_xlsx_helper_demo/views/res_partner.xml | 19 + 40 files changed, 2597 insertions(+) create mode 100644 account_move_line_report_xls/README.rst create mode 100644 account_move_line_report_xls/__init__.py create mode 100644 account_move_line_report_xls/__manifest__.py create mode 100644 account_move_line_report_xls/i18n/fr.po create mode 100644 account_move_line_report_xls/i18n/nl.po create mode 100644 account_move_line_report_xls/models/__init__.py create mode 100644 account_move_line_report_xls/models/account_move_line.py create mode 100644 account_move_line_report_xls/report/__init__.py create mode 100644 account_move_line_report_xls/report/account_move_line_xlsx.py create mode 100644 account_move_line_report_xls/report/account_move_line_xlsx.xml create mode 100644 account_move_line_report_xls/static/description/icon.png create mode 100644 account_move_line_report_xls/static/description/icon.svg create mode 100644 account_move_line_report_xls/static/description/index.html create mode 100644 account_move_line_report_xls/static/description/journal_items.png create mode 100644 account_move_line_report_xls/static/description/oca_logo.png create mode 100644 account_move_line_report_xls/tests/__init__.py create mode 100644 account_move_line_report_xls/tests/test_aml_report_xlsx.py create mode 100644 report_xlsx_helper/README.rst create mode 100644 report_xlsx_helper/__init__.py create mode 100644 report_xlsx_helper/__manifest__.py create mode 100644 report_xlsx_helper/controllers/__init__.py create mode 100644 report_xlsx_helper/controllers/main.py create mode 100644 report_xlsx_helper/i18n/report_xlsx_helper.pot create mode 100644 report_xlsx_helper/models/__init__.py create mode 100644 report_xlsx_helper/models/ir_actions_report.py create mode 100644 report_xlsx_helper/report/__init__.py create mode 100644 report_xlsx_helper/report/report_xlsx_abstract.py create mode 100644 report_xlsx_helper/report/test_partner_report_xlsx.py create mode 100644 report_xlsx_helper/static/description/icon.png create mode 100644 report_xlsx_helper/tests/__init__.py create mode 100644 report_xlsx_helper/tests/test_report_xlsx_helper.py create mode 100644 report_xlsx_helper_demo/README.rst create mode 100644 report_xlsx_helper_demo/__init__.py create mode 100644 report_xlsx_helper_demo/__manifest__.py create mode 100644 report_xlsx_helper_demo/models/__init__.py create mode 100644 report_xlsx_helper_demo/models/res_partner.py create mode 100644 report_xlsx_helper_demo/report/__init__.py create mode 100644 report_xlsx_helper_demo/report/partner_export_xlsx.py create mode 100644 report_xlsx_helper_demo/static/description/icon.png create mode 100644 report_xlsx_helper_demo/views/res_partner.xml diff --git a/account_move_line_report_xls/README.rst b/account_move_line_report_xls/README.rst new file mode 100644 index 00000000..0ce69ca0 --- /dev/null +++ b/account_move_line_report_xls/README.rst @@ -0,0 +1,91 @@ +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: https://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +========================== +Journal Items Excel Export +========================== + +This module extends the functionality of the journal items +('account.move.line') list view and allow you to export the selected lines. + +Installation +============ + +To install this module, you need also the **report_xlsx_helper** +module located in: + +https://github.com/OCA/reporting-engine + +Usage +===== + +To use this module, you need to: + +* go to the list view of the journal items +* select the lines you wish to export +* click on the button on top to export + +The Excel export can be tailored to your exact needs via the following methods +of the 'account.move.line' object: + +* **_report_xls_fields** + + Add/drop columns or change order from the list of columns that are defined + in the Excel template. + + The following fields are defined in the Excel template: + + move, name, date, journal, period, partner, account, + date_maturity, debit, credit, balance, + reconcile, reconcile_partial, analytic_account, + ref, partner_ref, tax_code, tax_amount, amount_residual, + amount_currency, currency_name, company_currency, + amount_residual_currency, product, product_ref', product_uom, quantity, + statement, invoice, narration, blocked + +* **_report_xls_template** + + Change/extend the Excel template. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/91/11.0 + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Author +------------ +* Luc De Meyer, Noviat + +Contributors +------------ +* Guillaume Auger + +Icon +---- +* https://openclipart.org/detail/38353/58294main-The.Brain.in.Space-page-91-spreadsheet-by-rejon + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit http://odoo-community.org. diff --git a/account_move_line_report_xls/__init__.py b/account_move_line_report_xls/__init__.py new file mode 100644 index 00000000..bb05a243 --- /dev/null +++ b/account_move_line_report_xls/__init__.py @@ -0,0 +1,8 @@ +from . import models +try: + from . import report +except ImportError: + import logging + logging.getLogger('odoo.module').\ + warning('''report_xlsx_helper not available in addons path. + account_move_line_report_xls will not be usable''') diff --git a/account_move_line_report_xls/__manifest__.py b/account_move_line_report_xls/__manifest__.py new file mode 100644 index 00000000..f33e118f --- /dev/null +++ b/account_move_line_report_xls/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2009-2018 Noviat. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Account Move Line XLSX export', + 'version': '11.0.1.0.0', + 'license': 'AGPL-3', + 'author': 'Noviat,' + 'Odoo Community Association (OCA)', + 'category': 'Accounting & Finance', + 'summary': 'Journal Items Excel export', + 'depends': ['account', 'report_xlsx_helper'], + 'data': [ + 'report/account_move_line_xlsx.xml', + ], + 'installable': True, +} diff --git a/account_move_line_report_xls/i18n/fr.po b/account_move_line_report_xls/i18n/fr.po new file mode 100644 index 00000000..0e8e8639 --- /dev/null +++ b/account_move_line_report_xls/i18n/fr.po @@ -0,0 +1,181 @@ +# French translation of Odoo. +# This file contains the translation of the following modules: +# * account_move_line_report_xls +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: support@noviat.com\n" +"POT-Creation-Date: 2016-05-16 12:31:16.568000\n" +"PO-Revision-Date: 2016-05-16 12:31:16.568000\n" +"Last-Translator: Luc De Meyer (Noviat nv/sa)\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Journal Items" +msgstr "Écritures comptables" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Entry" +msgstr "Écriture" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Name" +msgstr "Nom" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Reference" +msgstr "Référence" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Effective Date" +msgstr "Date" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Period" +msgstr "Période" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Partner" +msgstr "Partenaire" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Partner Reference" +msgstr "Réf. Partenaire" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Account" +msgstr "Compte" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Maturity Date" +msgstr "Date d'échéance" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Debit" +msgstr "Débit" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Credit" +msgstr "Crédit" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Balance" +msgstr "Solde" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Rec." +msgstr "Let." + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Part. Rec." +msgstr "Let. Part." + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Tax Code" +msgstr "Case TVA" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Tax/Base Amount" +msgstr "Montant TVA/Base" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Am. Currency" +msgstr "Montant devise" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Curr." +msgstr "Dev." + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Journal" +msgstr "Journal" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Comp. Curr." +msgstr "Dev. Soc." + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Analytic Account" +msgstr "Compte analytique" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Analytic Account Reference" +msgstr "Référence compte analytique" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Product" +msgstr "Article" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Product Reference" +msgstr "Réf. Article" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Unit of Measure" +msgstr "Unité de mesure" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Qty" +msgstr "Qty" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Statement" +msgstr "Relevé" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Invoice" +msgstr "Facture" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Residual Amount" +msgstr "Montant résiduel" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Res. Am. in Curr." +msgstr "Montant résiduel en dev." + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Notes" +msgstr "Notes" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Lit." +msgstr "Lit." + diff --git a/account_move_line_report_xls/i18n/nl.po b/account_move_line_report_xls/i18n/nl.po new file mode 100644 index 00000000..fed06d5c --- /dev/null +++ b/account_move_line_report_xls/i18n/nl.po @@ -0,0 +1,181 @@ +# Dutch translation of Odoo. +# This file contains the translation of the following modules: +# * account_move_line_report_xls +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: support@noviat.com\n" +"POT-Creation-Date: 2016-05-16 12:31:16.564000\n" +"PO-Revision-Date: 2016-05-16 12:31:16.564000\n" +"Last-Translator: Luc De Meyer (Noviat nv/sa)\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Journal Items" +msgstr "Boekingsregels" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Entry" +msgstr "Boeking" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Name" +msgstr "Naam" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Reference" +msgstr "Referentie" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Effective Date" +msgstr "Datum" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Period" +msgstr "Periode" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Partner" +msgstr "Partner" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Partner Reference" +msgstr "Ref. Partner" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Account" +msgstr "Rekening" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Maturity Date" +msgstr "Vervaldatum" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Debit" +msgstr "Debet" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Credit" +msgstr "Credit" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Balance" +msgstr "Saldo" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Rec." +msgstr "Rec." + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Part. Rec." +msgstr "Rec. Part." + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Tax Code" +msgstr "BTW vak" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Tax/Base Amount" +msgstr "Bedrag BTW/Mvh" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Am. Currency" +msgstr "Bedrag valuta" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Curr." +msgstr "Val." + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Journal" +msgstr "Dagboek" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Comp. Curr." +msgstr "Bedr. Val." + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Analytic Account" +msgstr "Kostenplaats" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Analytic Account Reference" +msgstr "Kostenplaats referentie" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Product" +msgstr "Product" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Product Reference" +msgstr "Ref. Product" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Unit of Measure" +msgstr "Maateenheid" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Qty" +msgstr "HvH" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Statement" +msgstr "Uitreksel" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Invoice" +msgstr "Factuur" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Residual Amount" +msgstr "Restbedrag" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Res. Am. in Curr." +msgstr "Restbedrag in val." + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Notes" +msgstr "Notities" + +#. module: account_move_line_report_xls +#: report:move.line.list.xls:0 +msgid "Lit." +msgstr "Lit." + diff --git a/account_move_line_report_xls/models/__init__.py b/account_move_line_report_xls/models/__init__.py new file mode 100644 index 00000000..8795b3be --- /dev/null +++ b/account_move_line_report_xls/models/__init__.py @@ -0,0 +1 @@ +from . import account_move_line diff --git a/account_move_line_report_xls/models/account_move_line.py b/account_move_line_report_xls/models/account_move_line.py new file mode 100644 index 00000000..3539d09b --- /dev/null +++ b/account_move_line_report_xls/models/account_move_line.py @@ -0,0 +1,48 @@ +# Copyright 2009-2018 Noviat. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, models +from odoo.addons.report_xlsx_helper.report.report_xlsx_abstract \ + import ReportXlsxAbstract +_render = ReportXlsxAbstract._render + + +class AccountMoveLine(models.Model): + _inherit = 'account.move.line' + + # Change list in custom module e.g. to add/drop columns or change order + @api.model + def _report_xlsx_fields(self): + return [ + 'move', 'name', 'date', 'journal', 'partner', 'account', + 'date_maturity', 'debit', 'credit', 'balance', + 'full_reconcile', 'reconcile_amount', + # 'analytic_account_name', 'analytic_account', + # 'ref', 'partner_ref', + # 'amount_residual', 'amount_currency', 'currency_name', + # 'company_currency', 'amount_residual_currency', + # 'product', 'product_ref', 'product_uom', 'quantity', + # 'statement', 'invoice', 'narration', 'blocked', + # 'id', 'matched_debit_ids', 'matched_credit_ids', + ] + + # Change/Add Template entries + @api.model + def _report_xlsx_template(self): + """ + Template updates, e.g. + + my_change = { + 'move': { + 'header': { + 'value': 'My Move Title', + }, + 'lines': { + 'value': _render("line.move_id.name or ''"), + }, + 'width': 20, + }, + } + return my_change + """ + return {} diff --git a/account_move_line_report_xls/report/__init__.py b/account_move_line_report_xls/report/__init__.py new file mode 100644 index 00000000..dc9f47cd --- /dev/null +++ b/account_move_line_report_xls/report/__init__.py @@ -0,0 +1 @@ +from . import account_move_line_xlsx diff --git a/account_move_line_report_xls/report/account_move_line_xlsx.py b/account_move_line_report_xls/report/account_move_line_xlsx.py new file mode 100644 index 00000000..c107c329 --- /dev/null +++ b/account_move_line_report_xls/report/account_move_line_xlsx.py @@ -0,0 +1,444 @@ +# Copyright 2009-2018 Noviat +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import models +from odoo.tools.translate import translate + +_logger = logging.getLogger(__name__) + +IR_TRANSLATION_NAME = 'move.line.list.xls' + + +class AccountMoveLineXlsx(models.AbstractModel): + _name = 'report.account_move_line_report_xls.account_move_line_xlsx' + _inherit = 'report.report_xlsx.abstract' + + def _(self, src): + lang = self.env.context.get('lang', 'en_US') + val = translate( + self.env.cr, IR_TRANSLATION_NAME, 'report', lang, src) or src + return val + + def _get_ws_params(self, workbook, data, amls): + + # XLSX Template + col_specs = { + 'move': { + 'header': { + 'value': self._('Entry'), + }, + 'lines': { + 'value': self._render("line.move_id.name"), + }, + 'width': 20, + }, + 'name': { + 'header': { + 'value': self._('Name'), + }, + 'lines': { + 'value': self._render("line.name"), + }, + 'width': 42, + }, + 'ref': { + 'header': { + 'value': self._('Reference'), + }, + 'lines': { + 'value': self._render("line.ref"), + }, + 'width': 42, + }, + 'date': { + 'header': { + 'value': self._('Effective Date'), + }, + 'lines': { + 'value': self._render( + "datetime.strptime(line.date, '%Y-%m-%d')"), + 'format': self.format_tcell_date_left, + }, + 'width': 13, + }, + 'partner': { + 'header': { + 'value': self._('Partner'), + }, + 'lines': { + 'value': self._render( + "line.partner_id and line.partner_id.name"), + }, + 'width': 36, + }, + 'partner_ref': { + 'header': { + 'value': self._('Partner Reference'), + }, + 'lines': { + 'value': self._render( + "line.partner_id and line.partner_id.ref"), + }, + 'width': 36, + }, + 'account': { + 'header': { + 'value': self._('Account'), + }, + 'lines': { + 'value': self._render( + "line.account_id.code"), + }, + 'width': 12, + }, + 'date_maturity': { + 'header': { + 'value': self._('Maturity Date'), + }, + 'lines': { + 'value': self._render( + "datetime.strptime(line.date_maturity,'%Y-%m-%d')"), + 'format': self.format_tcell_date_left, + }, + 'width': 13, + }, + 'debit': { + 'header': { + 'value': self._('Debit'), + 'format': self.format_theader_yellow_right, + }, + 'lines': { + 'value': self._render("line.debit"), + 'format': self.format_tcell_amount_right, + }, + 'totals': { + 'type': 'formula', + 'value': self._render("debit_formula"), + 'format': self.format_theader_yellow_amount_right, + }, + 'width': 18, + }, + 'credit': { + 'header': { + 'value': self._('Credit'), + 'format': self.format_theader_yellow_right, + }, + 'lines': { + 'value': self._render("line.credit"), + 'format': self.format_tcell_amount_right, + }, + 'totals': { + 'type': 'formula', + 'value': self._render("credit_formula"), + 'format': self.format_theader_yellow_amount_right, + }, + 'width': 18, + }, + 'balance': { + 'header': { + 'value': self._('Balance'), + 'format': self.format_theader_yellow_right, + }, + 'lines': { + 'value': self._render("line.balance"), + 'format': self.format_tcell_amount_right, + }, + 'totals': { + 'type': 'formula', + 'value': self._render("bal_formula"), + 'format': self.format_theader_yellow_amount_right, + }, + 'width': 18, + }, + 'full_reconcile': { + 'header': { + 'value': self._('Rec.'), + 'format': self.format_theader_yellow_center, + }, + 'lines': { + 'value': self._render( + "line.full_reconcile_id " + "and line.full_reconcile_id.name"), + 'format': self.format_tcell_center, + }, + 'width': 12, + }, + 'reconcile_amount': { + 'header': { + 'value': self._('Reconcile Amount'), + }, + 'lines': { + 'value': self._render( + "line.full_reconcile_id and line.balance or " + "(sum(line.matched_credit_ids.mapped('amount')) - " + "sum(line.matched_debit_ids.mapped('amount')))"), + 'format': self.format_tcell_amount_right, + }, + 'width': 12, + }, + 'matched_debit_ids': { + 'header': { + 'value': self._('Matched Debits'), + }, + 'lines': { + 'value': self._render( + "line.matched_debit_ids " + "and str([x.debit_move_id.id " + "for x in line.matched_debit_ids])"), + }, + 'width': 20, + }, + 'matched_credit_ids': { + 'header': { + 'value': self._('Matched Credits'), + }, + 'lines': { + 'value': self._render( + "line.matched_credit_ids " + "and str([x.credit_move_id.id " + "for x in line.matched_credit_ids])"), + }, + 'width': 20, + }, + 'amount_currency': { + 'header': { + 'value': self._('Am. Currency'), + 'format': self.format_theader_yellow_right, + }, + 'lines': { + 'value': self._render("line.amount_currency"), + 'format': self.format_tcell_amount_right, + }, + 'width': 18, + }, + 'currency_name': { + 'header': { + 'value': self._('Curr.'), + 'format': self.format_theader_yellow_center, + }, + 'lines': { + 'value': self._render( + "line.currency_id and line.currency_id.name"), + 'format': self.format_tcell_center, + }, + 'width': 6, + }, + 'journal': { + 'header': { + 'value': self._('Journal'), + }, + 'lines': { + 'value': self._render("line.journal_id.code"), + }, + 'width': 12, + }, + 'company_currency': { + 'header': { + 'value': self._('Comp. Curr.'), + 'format': self.format_theader_yellow_center, + }, + 'lines': { + 'value': self._render( + "line.company_id.currency_id.name"), + 'format': self.format_tcell_center, + }, + 'width': 10, + }, + 'analytic_account': { + 'header': { + 'value': self._('Analytic Account Reference'), + }, + 'lines': { + 'value': self._render( + "line.analytic_account_id " + "and line.analytic_account_id.code"), + }, + 'width': 36, + }, + 'analytic_account_name': { + 'header': { + 'value': self._('Analytic Account'), + }, + 'lines': { + 'value': self._render( + "line.analytic_account_id " + "and line.analytic_account_id.name"), + }, + 'width': 36, + }, + 'product': { + 'header': { + 'value': self._('Product'), + }, + 'lines': { + 'value': self._render( + "line.product_id and line.product_id.name"), + }, + 'width': 36, + }, + 'product_ref': { + 'header': { + 'value': self._('Product Reference'), + }, + 'lines': { + 'value': self._render( + "line.product_id and line.product_id.default_code " + "or ''"), + }, + 'width': 36, + }, + 'product_uom': { + 'header': { + 'value': self._('Unit of Measure'), + }, + 'lines': { + 'value': self._render( + "line.product_uom_id and line.product_uom_id.name"), + }, + 'width': 20, + }, + 'quantity': { + 'header': { + 'value': self._('Qty'), + 'format': self.format_theader_yellow_right, + }, + 'lines': { + 'value': self._render("line.quantity"), + 'format': self.format_tcell_amount_right, + }, + 'width': 8, + }, + 'statement': { + 'header': { + 'value': self._('Statement'), + }, + 'lines': { + 'value': self._render( + "line.statement_id and line.statement_id.name"), + }, + 'width': 20, + }, + 'invoice': { + 'header': { + 'value': self._('Invoice'), + }, + 'lines': { + 'value': self._render( + "line.invoice_id and line.invoice_id.number"), + }, + 'width': 20, + }, + 'amount_residual': { + 'header': { + 'value': self._('Residual Amount'), + 'format': self.format_theader_yellow_right, + }, + 'lines': { + 'value': self._render("line.amount_residual"), + 'format': self.format_tcell_amount_right, + }, + 'width': 18, + }, + 'amount_residual_currency': { + 'header': { + 'value': self._('Res. Am. in Curr.'), + 'format': self.format_theader_yellow_right, + }, + 'lines': { + 'value': self._render("line.amount_residual_currency"), + 'format': self.format_tcell_amount_right, + }, + 'width': 18, + }, + 'narration': { + 'header': { + 'value': self._('Notes'), + }, + 'lines': { + 'value': self._render("line.move_id.narration or ''"), + }, + 'width': 42, + }, + 'blocked': { + 'header': { + 'value': self._('Lit.'), + 'format': self.format_theader_yellow_center, + }, + 'lines': { + 'value': self._render("line.blocked and 'x' or ''"), + 'format': self.format_tcell_center, + }, + 'width': 4, + }, + 'id': { + 'header': { + 'value': self._('Id'), + 'format': self.format_theader_yellow_right, + }, + 'lines': { + 'value': self._render("line.id"), + 'format': self.format_tcell_integer_right, + }, + 'width': 12, + }, + } + col_specs.update(self.env['account.move.line']._report_xlsx_template()) + wanted_list = self.env['account.move.line']._report_xlsx_fields() + title = self._("Journal Items") + + return [{ + 'ws_name': title, + 'generate_ws_method': '_amls_export', + 'title': title, + 'wanted_list': wanted_list, + 'col_specs': col_specs, + }] + + def _amls_export(self, workbook, ws, ws_params, data, amls): + + ws.set_landscape() + ws.fit_to_pages(1, 0) + ws.set_header(self.xls_headers['standard']) + ws.set_footer(self.xls_footers['standard']) + + self._set_column_width(ws, ws_params) + + row_pos = 0 + row_pos = self._write_ws_title(ws, row_pos, ws_params) + + row_pos = self._write_line( + ws, row_pos, ws_params, col_specs_section='header', + default_format=self.format_theader_yellow_left) + + ws.freeze_panes(row_pos, 0) + + wanted_list = ws_params['wanted_list'] + debit_pos = 'debit' in wanted_list and wanted_list.index('debit') + credit_pos = 'credit' in wanted_list and wanted_list.index('credit') + + for line in amls: + row_pos = self._write_line( + ws, row_pos, ws_params, col_specs_section='lines', + render_space={'line': line}, + default_format=self.format_tcell_left) + + aml_cnt = len(amls) + debit_start = self._rowcol_to_cell(row_pos - aml_cnt, debit_pos) + debit_stop = self._rowcol_to_cell(row_pos - 1, debit_pos) + debit_formula = 'SUM(%s:%s)' % (debit_start, debit_stop) + credit_start = self._rowcol_to_cell(row_pos - aml_cnt, credit_pos) + credit_stop = self._rowcol_to_cell(row_pos - 1, credit_pos) + credit_formula = 'SUM(%s:%s)' % (credit_start, credit_stop) + debit_cell = self._rowcol_to_cell(row_pos, debit_pos) + credit_cell = self._rowcol_to_cell(row_pos, credit_pos) + bal_formula = debit_cell + '-' + credit_cell + row_pos = self._write_line( + ws, row_pos, ws_params, col_specs_section='totals', + render_space={ + 'debit_formula': debit_formula, + 'credit_formula': credit_formula, + 'bal_formula': bal_formula, + }, + default_format=self.format_theader_yellow_left) diff --git a/account_move_line_report_xls/report/account_move_line_xlsx.xml b/account_move_line_report_xls/report/account_move_line_xlsx.xml new file mode 100644 index 00000000..3f55dffb --- /dev/null +++ b/account_move_line_report_xls/report/account_move_line_xlsx.xml @@ -0,0 +1,15 @@ + + + + + Export Selected Lines + account.move.line + ir.actions.report + action + + account_move_line_report_xls.account_move_line_xlsx + account_move_line + xlsx + + + diff --git a/account_move_line_report_xls/static/description/icon.png b/account_move_line_report_xls/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9fede7ebf1d4336c58688765cd73bf6eaea5e966 GIT binary patch literal 4723 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 A)&Kwi literal 0 HcmV?d00001 diff --git a/account_move_line_report_xls/static/description/icon.svg b/account_move_line_report_xls/static/description/icon.svg new file mode 100644 index 00000000..32210f51 --- /dev/null +++ b/account_move_line_report_xls/static/description/icon.svg @@ -0,0 +1,320 @@ + + + + + + + + spain_provinces + + + + spain + geography + + + + + sherrera + + + + + sherrera + + + + + sherrera + + + + image/svg+xml + + + en + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/account_move_line_report_xls/static/description/index.html b/account_move_line_report_xls/static/description/index.html new file mode 100644 index 00000000..ed6e2b8e --- /dev/null +++ b/account_move_line_report_xls/static/description/index.html @@ -0,0 +1,108 @@ + + +
+
+
+

Journal Items Excel export

+
+
+
+ +
+
+
+

+ This module extends the functionality of the journal items ('account.move.line') list view and allow you to export the selected lines. +

+
+
+
+ +
+
+
+

Installation & Usage

+

+ To install this module, you need also the report_xls module located in: +
https://github.com/OCA/reporting-engine +

+

+ To use this module, you need to: +

    +
  • go to the list view of the journal items
  • +
  • select the lines you wish to export
  • +
  • click on the button on top to export
  • +
+

+

+ The Excel export can be tailored to your exact needs via the following methods of the 'account.move.line' object: +

    +
  • + _report_xlsx_fields +
    + Add/drop columns or change order from the list of columns that are defined in the Excel template. +
    + The following fields are available: +
    + + move, name, date, journal, partner, account, +
    + date_maturity, debit, credit, balance, +
    + reconcile, analytic_account, +
    + ref, partner_ref, amount_residual, +
    + amount_currency, currency_name, company_currency, +
    + amount_residual_currency, product, product_ref', product_uom, quantity, +
    + statement, invoice, narration, blocked +
    +
    +
    +
  • +
+

+

+

    +
  • + _report_xlsx_template +
    + Change/extend the Excel template. +
    +
  • +
+

+
+
+
+ +
+
+
+

Maintainer

+
+
+

+ This module is maintained by the OCA. +

+

+ OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development + of Odoo features and promote its widespread use. +

+

+ To contribute to this module, please visit http://odoo-community.org. +

+
+ +
+
+ +
diff --git a/account_move_line_report_xls/static/description/journal_items.png b/account_move_line_report_xls/static/description/journal_items.png new file mode 100644 index 0000000000000000000000000000000000000000..0dcacd2bc35f464c23b0bd7690f58d89caf0f93b GIT binary patch literal 46725 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%0Aq_oRt=8ZPytWJte(v{V)-5;a{yOc8lMxG_%cH zF^0rE6ow+bh_eNm2g{3poa(KmNqYd^-um`0Uj7J51@LII0AxG`uQm4OcfV7qTygZt z!MMl|tNshkh75IsO0fyZx3e=2RU=C*uqZFCdOc|C4ZaC z<_R~ktwXX@rD(j$bWST=`nEnSFE&NT}c4O#u< zfpjxQBQ--B;N(}P=UZHO=c`dNe<* zC!*W+Xye^4gB)E&Qg2zf0>E!RaO3cnus5&vh?e;IE&z4UZpt{^PCZJ% zv|k+;gzMS>^JF2Q&I83!Y`+U+&*DseuKj_sF1c3M5jnpkFh&|skq0EB?!^+^8` zl8YozrCaCaI?p|o50miu1VFfq8_ti)&I2Sp71L~w+X zWhPpKxvJxtuh`ixbZFq3{z2QJ9NL@soi#)A@lDOhvIoE;b1LLhUe`vu-wrNVt-ra} z`QZ<_I8D(A|DJB9Go>dze?qai(F^<=Cls_($ut4NqfX!(jdQJo{LVRIZ6&zOcvyYM z=OS|cY66=ERsl0)OplXt=5aAw7~w0+q5YY{&e4O?Pq<;VE4R5lUk(6mDE2;a{G`dh zBrc@Ap7n%v%Lb`a+d7hn$Ck>1$oO?Veu~d3SLX_0Q76U z#^4mS+kO%P3W)VB+X$nNsX!zT2wfy{7;gS47YAgb!&>4HzAiXlby3{G{TRTyyP!1t zJ<+5JK2%EG_hh|Ea90TbQlb7U-;pA^)c-+2gH`L*Rn2IR zr~49P53&Z{g$)?+*1WEgUn}|mAnj!D&Z-v%!W&*fpUq!Dabqh;`mLLfBRV(h_r{F* zjWGaw^9mEz5(@9Jrag)tfK;0q^vUW@3Od0zde6d65}WN61b@KC&8uhd2)pMjD&e z$JSA=)aQglE+RMq8R+T!psLOdSKE;v#5-xy!jk|@hK<~+ak}sqO8$}ZbkPKmuQ1F2 z@|D?7kzTS9N3tFtHJF=2tCt;qPTYN}_X8*&@>H)hm|mcLqrC;hGW@=f>S@2kCmYQv zNAs`o;Y}&-cm3uo^2LW$B1mc1^I%R9oj7RoWkdb%M`r6BlQBkJKBXDEtAMhZU#~Xl zaR8qXm_?gqL~GJ|?v-B0q(06)-Qm&MbD?z}J(irv8_#ckFMmBP7VMi@vPdil72PPF zb4v?`opvUlmv;_RvA^og{1Sp>G*SZQmb+WEaxSyE4L2Dl#QRplD44v%5xl5hmKAo+ zIc!Rr#nKV+=qdNylu3Y9dJ9Rl0BKJwnp)xP0!Zrl;IY8z z)i?36*EevPMAai{^X_B!{(U)yKnzfjd3D#kvxmoWoFExDC zNLW_9>o;R`(Ics2tu8cs%!(Fv-BKqxufvP%=@_6i*<%rFQqg@*3K2%`>#1Cemt;C* zoQ3=IKhujG z(`dmknH1=P@_KTUYQ{<{&IaAvRQPB;ET&os`6g_j=lR0!I|w)0(%SoBQRxf8=PVN= z%1lNSjc1G_0X50)|Ha;WMm4>zd%G$MIuQ_Y(u)FuC`F2dgr

qTob8ih|NZ3%#m{ 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 literal 0 HcmV?d00001 diff --git a/account_move_line_report_xls/tests/__init__.py b/account_move_line_report_xls/tests/__init__.py new file mode 100644 index 00000000..f1626769 --- /dev/null +++ b/account_move_line_report_xls/tests/__init__.py @@ -0,0 +1 @@ +from . import test_aml_report_xlsx diff --git a/account_move_line_report_xls/tests/test_aml_report_xlsx.py b/account_move_line_report_xls/tests/test_aml_report_xlsx.py new file mode 100644 index 00000000..462e6dec --- /dev/null +++ b/account_move_line_report_xls/tests/test_aml_report_xlsx.py @@ -0,0 +1,36 @@ +# Copyright 2009-2018 Noviat. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests.common import TransactionCase + + +class TestAmlReportXlsx(TransactionCase): + + def setUp(self): + super(TestAmlReportXlsx, self).setUp() + self.report = self.env.ref( + 'account_move_line_report_xls.action_account_move_line_xlsx') + sale_journal = self.env['account.journal'].search( + [('type', '=', 'sale')])[0] + ar = self.env['account.account'].search( + [('internal_type', '=', 'receivable')])[0] + aml_vals = [ + {'name': 'debit', + 'debit': 100, + 'account_id': ar.id, + }, + {'name': 'credit', + 'credit': 100, + 'account_id': ar.id, + }, + ] + am = self.env['account.move'].create({ + 'name': 'test', + 'journal_id': sale_journal.id, + 'line_ids': [(0, 0, x) for x in aml_vals], + }) + self.amls = am.line_ids + + def test_aml_report_xlsx(self): + report_xls = self.report.render_xlsx(self.amls.ids, None) + self.assertEqual(report_xls[1], 'xlsx') diff --git a/report_xlsx_helper/README.rst b/report_xlsx_helper/README.rst new file mode 100644 index 00000000..7f368768 --- /dev/null +++ b/report_xlsx_helper/README.rst @@ -0,0 +1,88 @@ +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: https://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +=========================== +Excel report engine helpers +=========================== + +This module provides a set of tools to facilitate the creation of excel reports with format xlsx. + +Usage +===== + +In order to create an Excel report you can define a report of type 'xlsx' in a static or dynamic way: + +* Static syntax: cf. ``account_move_line_report_xls`` for an example. +* Dynamic syntax: cf. ``report_xlsx_helper_demo`` for an example + +The ``AbstractReportXlsx`` class contains a number of attributes and methods to +facilitate the creation excel reports in Odoo. + +* Cell types + + string, number, boolean, datetime. + +* Cell formats + + The predefined cell formats result in a consistent + look and feel of the Odoo Excel reports. + +* Cell formulas + + Cell formulas can be easily added with the help of the ``_rowcol_to_cell()`` method. + +* Excel templates + + It is possible to define Excel templates which can be adapted + by 'inherited' modules. + Download the ``account_move_line_report_xls`` module + from http://apps.odoo.com as example. + +* Excel with multiple sheets + + Download the ``account_asset_management_xls`` module + from http://apps.odoo.com as example. + +Installation +============ + +This module requires report_xlsx version 11.0.1.03 or higher. + +Configuration and Usage +======================= + +.. 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/11.0 + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Luc De Meyer + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit http://odoo-community.org. diff --git a/report_xlsx_helper/__init__.py b/report_xlsx_helper/__init__.py new file mode 100644 index 00000000..9b6fa04e --- /dev/null +++ b/report_xlsx_helper/__init__.py @@ -0,0 +1,3 @@ +from . import controllers +from . import models +from . import report diff --git a/report_xlsx_helper/__manifest__.py b/report_xlsx_helper/__manifest__.py new file mode 100644 index 00000000..afd22481 --- /dev/null +++ b/report_xlsx_helper/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2009-2018 Noviat. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Report xlsx helpers', + 'author': 'Noviat,' + 'Odoo Community Association (OCA)', + 'website': 'https://github.com/OCA/reporting-engine', + 'category': 'Reporting', + 'version': '11.0.1.0.0', + 'license': 'AGPL-3', + 'depends': [ + 'report_xlsx', + ], + 'installable': True, +} diff --git a/report_xlsx_helper/controllers/__init__.py b/report_xlsx_helper/controllers/__init__.py new file mode 100644 index 00000000..12a7e529 --- /dev/null +++ b/report_xlsx_helper/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/report_xlsx_helper/controllers/main.py b/report_xlsx_helper/controllers/main.py new file mode 100644 index 00000000..abaf974d --- /dev/null +++ b/report_xlsx_helper/controllers/main.py @@ -0,0 +1,54 @@ +# Copyright 2009-2018 Noviat. +# License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html). + +import json + +from odoo.addons.report_xlsx.controllers.main import ReportController +from odoo.http import content_disposition, route, request + + +class ReportController(ReportController): + + @route([ + '/report//', + '/report///', + ], type='http', auth='user', website=True) + def report_routes(self, reportname, docids=None, converter=None, **data): + report = request.env['ir.actions.report']._get_report_from_name( + reportname) + if converter == 'xlsx' and not report: + + context = dict(request.env.context) + if docids: + docids = [int(i) for i in docids.split(',')] + if data.get('options'): + data.update(json.loads(data.pop('options'))) + if data.get('context'): + # Ignore 'lang' here, because the context in data is the one + # from the webclient *but* if the user explicitely wants to + # change the lang, this mechanism overwrites it. + data['context'] = json.loads(data['context']) + if data['context'].get('lang'): + del data['context']['lang'] + context.update(data['context']) + context['report_name'] = reportname + + xlsx = report.with_context(context).render_xlsx( + docids, data=data + )[0] + report_file = context.get('report_file') + if not report_file: + active_model = context.get('active_model', 'export') + report_file = active_model.replace('.', '_') + xlsxhttpheaders = [ + ('Content-Type', 'application/vnd.openxmlformats-' + 'officedocument.spreadsheetml.sheet'), + ('Content-Length', len(xlsx)), + ( + 'Content-Disposition', + content_disposition(report_file + '.xlsx') + ) + ] + return request.make_response(xlsx, headers=xlsxhttpheaders) + return super(ReportController, self).report_routes( + reportname, docids, converter, **data) diff --git a/report_xlsx_helper/i18n/report_xlsx_helper.pot b/report_xlsx_helper/i18n/report_xlsx_helper.pot new file mode 100644 index 00000000..81aed537 --- /dev/null +++ b/report_xlsx_helper/i18n/report_xlsx_helper.pot @@ -0,0 +1,67 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * report_xlsx_helper +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: report_xlsx_helper +#: code:addons/report_xlsx_helper/report/abstract_report_xlsx.py:520 +#, python-format +msgid "%s, _write_line : programming error detected while processing col_specs_section %s, column %s" +msgstr "" + +#. module: report_xlsx_helper +#: code:addons/report_xlsx_helper/report/abstract_report_xlsx.py:526 +#, python-format +msgid ", cellvalue %s" +msgstr "" + +#. module: report_xlsx_helper +#: code:addons/report_xlsx_helper/report/abstract_report_xlsx.py:52 +#, python-format +msgid "Programming Error:\n" +"\n" +"Excel Sheet name '%s' contains unsupported special characters: '%s'." +msgstr "" + +#. module: report_xlsx_helper +#: code:addons/report_xlsx_helper/report/abstract_report_xlsx.py:46 +#, python-format +msgid "Programming Error:\n" +"\n" +"Excel Sheet name '%s' should not exceed %s characters." +msgstr "" + +#. module: report_xlsx_helper +#: code:addons/report_xlsx_helper/report/abstract_report_xlsx.py:448 +#, python-format +msgid "Programming Error:\n" +"\n" +"The '%s' column is not defined in the worksheet column specifications." +msgstr "" + +#. module: report_xlsx_helper +#: code:addons/report_xlsx_helper/report/abstract_report_xlsx.py:489 +#, python-format +msgid "Programming Error:\n" +"\n" +"The '%s' column is not defined the worksheet column specifications." +msgstr "" + +#. module: report_xlsx_helper +#: code:addons/report_xlsx_helper/report/abstract_report_xlsx.py:462 +#, python-format +msgid "Programming Error:\n" +"\n" +"The 'title' parameter is mandatory when calling the '_write_ws_title' method." +msgstr "" + diff --git a/report_xlsx_helper/models/__init__.py b/report_xlsx_helper/models/__init__.py new file mode 100644 index 00000000..a248cf21 --- /dev/null +++ b/report_xlsx_helper/models/__init__.py @@ -0,0 +1 @@ +from . import ir_actions_report diff --git a/report_xlsx_helper/models/ir_actions_report.py b/report_xlsx_helper/models/ir_actions_report.py new file mode 100644 index 00000000..81db27d0 --- /dev/null +++ b/report_xlsx_helper/models/ir_actions_report.py @@ -0,0 +1,21 @@ +# Copyright 2009-2018 Noviat. +# License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html). + +from odoo import api, models, _ +from odoo.exceptions import UserError + + +class IrActionsReport(models.Model): + _inherit = 'ir.actions.report' + + @api.model + def render_xlsx(self, docids, data): + if not self and self.env.context.get('report_name'): + report_model_name = 'report.{}'.format( + self.env.context['report_name']) + report_model = self.env.get(report_model_name) + if report_model is None: + raise UserError( + _('%s model was not found' % report_model_name)) + return report_model.create_xlsx_report(docids, data) + return super(IrActionsReport, self).render_xlsx(docids, data) diff --git a/report_xlsx_helper/report/__init__.py b/report_xlsx_helper/report/__init__.py new file mode 100644 index 00000000..3222e9d5 --- /dev/null +++ b/report_xlsx_helper/report/__init__.py @@ -0,0 +1,2 @@ +from . import report_xlsx_abstract +from . import test_partner_report_xlsx diff --git a/report_xlsx_helper/report/report_xlsx_abstract.py b/report_xlsx_helper/report/report_xlsx_abstract.py new file mode 100644 index 00000000..b93cf2a4 --- /dev/null +++ b/report_xlsx_helper/report/report_xlsx_abstract.py @@ -0,0 +1,551 @@ +# Copyright 2009-2018 Noviat. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import datetime +import re +from types import CodeType +from xlsxwriter.utility import xl_rowcol_to_cell + +from odoo import fields, models, _ +from odoo.exceptions import UserError + + +class ReportXlsxAbstract(models.AbstractModel): + _inherit = 'report.report_xlsx.abstract' + + def generate_xlsx_report(self, workbook, data, objects): + self._define_formats(workbook) + for ws_params in self._get_ws_params(workbook, data, objects): + ws_name = ws_params.get('ws_name') + ws_name = self._check_ws_name(ws_name) + ws = workbook.add_worksheet(ws_name) + generate_ws_method = getattr( + self, ws_params['generate_ws_method']) + generate_ws_method(workbook, ws, ws_params, data, objects) + + def _check_ws_name(self, name, sanitize=True): + pattern = re.compile(r'[/\\*\[\]:?]') # invalid characters: /\*[]:? + max_chars = 31 + if sanitize: + # we could drop these two lines since a similar + # sanitize is done in tools.misc PatchedXlsxWorkbook + name = pattern.sub('', name) + name = name[:max_chars] + else: + if len(name) > max_chars: + raise UserError(_( + "Programming Error:\n\n" + "Excel Sheet name '%s' should not exceed %s characters." + ) % (name, max_chars)) + special_chars = pattern.findall(name) + if special_chars: + raise UserError(_( + "Programming Error:\n\n" + "Excel Sheet name '%s' contains unsupported special " + "characters: '%s'." + ) % (name, special_chars)) + return name + + def _get_ws_params(self, workbook, data, objects): + """ + Return list of dictionaries with parameters for the + worksheets. + + Keywords: + - 'generate_ws_method': mandatory + - 'ws_name': name of the worksheet + - 'title': title of the worksheet + - 'wanted_list': list of column names + - 'col_specs': cf. XXX + + The 'generate_ws_method' must be present in your report + and contain the logic to generate the content of the worksheet. + """ + return [] + + def _define_xls_headers(self, workbook): + """ + Predefined worksheet headers/footers. + """ + hf_params = { + 'font_size': 8, + 'font_style': 'I', # B: Bold, I: Italic, U: Underline + } + self.xls_headers = { + 'standard': '' + } + report_date = fields.Datetime.context_timestamp( + self.env.user, datetime.now()).strftime('%Y-%m-%d %H:%M') + self.xls_footers = { + 'standard': ( + '&L&%(font_size)s&%(font_style)s' + report_date + + '&R&%(font_size)s&%(font_style)s&P / &N' + ) % hf_params, + } + + def _define_formats(self, workbook): + """ + This section contains a number of pre-defined formats. + It is recommended to use these in order to have a + consistent look & feel between your XLSX reports. + """ + self._define_xls_headers(workbook) + + border_grey = '#D3D3D3' + border = {'border': True, 'border_color': border_grey} + theader = dict(border, bold=True) + bg_yellow = '#FFFFCC' + bg_blue = '#CCFFFF' + num_format = '#,##0.00' + num_format_conditional = '{0};[Red]-{0};{0}'.format(num_format) + pct_format = '#,##0.00%' + pct_format_conditional = '{0};[Red]-{0};{0}'.format(pct_format) + int_format = '#,##0' + int_format_conditional = '{0};[Red]-{0};{0}'.format(int_format) + date_format = 'YYYY-MM-DD' + theader_yellow = dict(theader, bg_color=bg_yellow) + theader_blue = dict(theader, bg_color=bg_blue) + + # format for worksheet title + self.format_ws_title = workbook.add_format( + {'bold': True, 'font_size': 14}) + + # no border formats + self.format_left = workbook.add_format({'align': 'left'}) + self.format_center = workbook.add_format({'align': 'center'}) + self.format_right = workbook.add_format({'align': 'right'}) + self.format_amount_left = workbook.add_format( + {'align': 'left', 'num_format': num_format}) + self.format_amount_center = workbook.add_format( + {'align': 'center', 'num_format': num_format}) + self.format_amount_right = workbook.add_format( + {'align': 'right', 'num_format': num_format}) + self.format_amount_conditional_left = workbook.add_format( + {'align': 'left', 'num_format': num_format_conditional}) + self.format_amount_conditional_center = workbook.add_format( + {'align': 'center', 'num_format': num_format_conditional}) + self.format_amount_conditional_right = workbook.add_format( + {'align': 'right', 'num_format': num_format_conditional}) + self.format_percent_left = workbook.add_format( + {'align': 'left', 'num_format': pct_format}) + self.format_percent_center = workbook.add_format( + {'align': 'center', 'num_format': pct_format}) + self.format_percent_right = workbook.add_format( + {'align': 'right', 'num_format': pct_format}) + self.format_percent_conditional_left = workbook.add_format( + {'align': 'left', 'num_format': pct_format_conditional}) + self.format_percent_conditional_center = workbook.add_format( + {'align': 'center', 'num_format': pct_format_conditional}) + self.format_percent_conditional_right = workbook.add_format( + {'align': 'right', 'num_format': pct_format_conditional}) + self.format_integer_left = workbook.add_format( + {'align': 'left', 'num_format': int_format}) + self.format_integer_center = workbook.add_format( + {'align': 'center', 'num_format': int_format}) + self.format_integer_right = workbook.add_format( + {'align': 'right', 'num_format': int_format}) + self.format_integer_conditional_left = workbook.add_format( + {'align': 'right', 'num_format': int_format_conditional}) + self.format_integer_conditional_center = workbook.add_format( + {'align': 'center', 'num_format': int_format_conditional}) + self.format_integer_conditional_right = workbook.add_format( + {'align': 'right', 'num_format': int_format_conditional}) + self.format_date_left = workbook.add_format( + {'align': 'left', 'num_format': date_format}) + self.format_date_center = workbook.add_format( + {'align': 'center', 'num_format': date_format}) + self.format_date_right = workbook.add_format( + {'align': 'right', 'num_format': date_format}) + + self.format_left_bold = workbook.add_format( + {'align': 'left', 'bold': True}) + self.format_center_bold = workbook.add_format( + {'align': 'center', 'bold': True}) + self.format_right_bold = workbook.add_format( + {'align': 'right', 'bold': True}) + self.format_amount_left_bold = workbook.add_format( + {'align': 'left', 'bold': True, 'num_format': num_format}) + self.format_amount_center_bold = workbook.add_format( + {'align': 'center', 'bold': True, 'num_format': num_format}) + self.format_amount_right_bold = workbook.add_format( + {'align': 'right', 'bold': True, 'num_format': num_format}) + self.format_amount_conditional_left_bold = workbook.add_format( + {'align': 'left', 'bold': True, + 'num_format': num_format_conditional}) + self.format_amount_conditional_center_bold = workbook.add_format( + {'align': 'center', 'bold': True, + 'num_format': num_format_conditional}) + self.format_amount_conditional_right_bold = workbook.add_format( + {'align': 'right', 'bold': True, + 'num_format': num_format_conditional}) + self.format_percent_left_bold = workbook.add_format( + {'align': 'left', 'bold': True, 'num_format': pct_format}) + self.format_percent_center_bold = workbook.add_format( + {'align': 'center', 'bold': True, 'num_format': pct_format}) + self.format_percent_right_bold = workbook.add_format( + {'align': 'right', 'bold': True, 'num_format': pct_format}) + self.format_percent_conditional_left_bold = workbook.add_format( + {'align': 'left', 'bold': True, + 'num_format': pct_format_conditional}) + self.format_percent_conditional_center_bold = workbook.add_format( + {'align': 'center', 'bold': True, + 'num_format': pct_format_conditional}) + self.format_percent_conditional_right_bold = workbook.add_format( + {'align': 'right', 'bold': True, + 'num_format': pct_format_conditional}) + self.format_integer_left_bold = workbook.add_format( + {'align': 'left', 'bold': True, 'num_format': int_format}) + self.format_integer_center_bold = workbook.add_format( + {'align': 'center', 'bold': True, 'num_format': int_format}) + self.format_integer_right_bold = workbook.add_format( + {'align': 'right', 'bold': True, 'num_format': int_format}) + self.format_integer_conditional_left_bold = workbook.add_format( + {'align': 'left', 'bold': True, + 'num_format': int_format_conditional}) + self.format_integer_conditional_center_bold = workbook.add_format( + {'align': 'center', 'bold': True, + 'num_format': int_format_conditional}) + self.format_integer_conditional_right_bold = workbook.add_format( + {'align': 'right', 'bold': True, + 'num_format': int_format_conditional}) + self.format_date_left_bold = workbook.add_format( + {'align': 'left', 'bold': True, 'num_format': date_format}) + self.format_date_center_bold = workbook.add_format( + {'align': 'center', 'bold': True, 'num_format': date_format}) + self.format_date_right_bold = workbook.add_format( + {'align': 'right', 'bold': True, 'num_format': date_format}) + + # formats for worksheet table column headers + self.format_theader_yellow_left = workbook.add_format(theader_yellow) + self.format_theader_yellow_center = workbook.add_format( + dict(theader_yellow, align='center')) + self.format_theader_yellow_right = workbook.add_format( + dict(theader_yellow, align='right')) + self.format_theader_yellow_amount_left = workbook.add_format( + dict(theader_yellow, num_format=num_format, align='left')) + self.format_theader_yellow_amount_center = workbook.add_format( + dict(theader_yellow, num_format=num_format, align='center')) + self.format_theader_yellow_amount_right = workbook.add_format( + dict(theader_yellow, num_format=num_format, align='right')) + + self.format_theader_yellow_amount_conditional_left = workbook.\ + add_format(dict(theader_yellow, num_format=num_format_conditional, + align='left')) + self.format_theader_yellow_amount_conditional_center = workbook.\ + add_format(dict(theader_yellow, num_format=num_format_conditional, + align='center')) + self.format_theader_yellow_amount_conditional_right = workbook.\ + add_format(dict(theader_yellow, num_format=num_format_conditional, + align='right')) + self.format_theader_yellow_percent_left = workbook.add_format( + dict(theader_yellow, num_format=pct_format, align='left')) + self.format_theader_yellow_percent_center = workbook.add_format( + dict(theader_yellow, num_format=pct_format, align='center')) + self.format_theader_yellow_percent_right = workbook.add_format( + dict(theader_yellow, num_format=pct_format, align='right')) + self.format_theader_yellow_percent_conditional_left = workbook.\ + add_format(dict(theader_yellow, num_format=pct_format_conditional, + align='left')) + self.format_theader_yellow_percent_conditional_center = workbook.\ + add_format(dict(theader_yellow, num_format=pct_format_conditional, + align='center')) + self.format_theader_yellow_percent_conditional_right = workbook.\ + add_format(dict(theader_yellow, num_format=pct_format_conditional, + align='right')) + self.format_theader_yellow_integer_left = workbook.add_format( + dict(theader_yellow, num_format=int_format, align='left')) + self.format_theader_yellow_integer_center = workbook.add_format( + dict(theader_yellow, num_format=int_format, align='center')) + self.format_theader_yellow_integer_right = workbook.add_format( + dict(theader_yellow, num_format=int_format, align='right')) + self.format_theader_yellow_integer_conditional_left = workbook.\ + add_format(dict(theader_yellow, num_format=int_format_conditional, + align='left')) + self.format_theader_yellow_integer_conditional_center = workbook.\ + add_format(dict(theader_yellow, num_format=int_format_conditional, + align='center')) + self.format_theader_yellow_integer_conditional_right = workbook.\ + add_format(dict(theader_yellow, num_format=int_format_conditional, + align='right')) + + self.format_theader_blue_left = workbook.add_format(theader_blue) + self.format_theader_blue_center = workbook.add_format( + dict(theader_blue, align='center')) + self.format_theader_blue_right = workbook.add_format( + dict(theader_blue, align='right')) + self.format_theader_blue_amount_left = workbook.add_format( + dict(theader_blue, num_format=num_format, align='left')) + self.format_theader_blue_amount_center = workbook.add_format( + dict(theader_blue, num_format=num_format, align='center')) + self.format_theader_blue_amount_right = workbook.add_format( + dict(theader_blue, num_format=num_format, align='right')) + self.format_theader_blue_amount_conditional_left = workbook.\ + add_format(dict(theader_blue, num_format=num_format_conditional, + align='left')) + self.format_theader_blue_amount_conditional_center = workbook.\ + add_format(dict(theader_blue, num_format=num_format_conditional, + align='center')) + self.format_theader_blue_amount_conditional_right = workbook.\ + add_format(dict(theader_blue, num_format=num_format_conditional, + align='right')) + self.format_theader_blue_percent_left = workbook.add_format( + dict(theader_blue, num_format=pct_format, align='left')) + self.format_theader_blue_percent_center = workbook.add_format( + dict(theader_blue, num_format=pct_format, align='center')) + self.format_theader_blue_percent_right = workbook.add_format( + dict(theader_blue, num_format=pct_format, align='right')) + self.format_theader_blue_percent_conditional_left = workbook.\ + add_format(dict(theader_blue, num_format=pct_format_conditional, + align='left')) + self.format_theader_blue_percent_conditional_center = workbook.\ + add_format(dict(theader_blue, num_format=pct_format_conditional, + align='center')) + self.format_theader_blue_percent_conditional_right = workbook.\ + add_format(dict(theader_blue, num_format=pct_format_conditional, + align='right')) + self.format_theader_blue_integer_left = workbook.add_format( + dict(theader_blue, num_format=int_format, align='left')) + self.format_theader_blue_integer_center = workbook.add_format( + dict(theader_blue, num_format=int_format, align='center')) + self.format_theader_blue_integer_right = workbook.add_format( + dict(theader_blue, num_format=int_format, align='right')) + self.format_theader_blue_integer_conditional_left = workbook.\ + add_format(dict(theader_blue, num_format=int_format_conditional, + align='left')) + self.format_theader_blue_integer_conditional_center = workbook.\ + add_format(dict(theader_blue, num_format=int_format_conditional, + align='center')) + self.format_theader_blue_integer_conditional_right = workbook.\ + add_format(dict(theader_blue, num_format=int_format_conditional, + align='right')) + + # formats for worksheet table cells + self.format_tcell_left = workbook.add_format( + dict(border, align='left')) + self.format_tcell_center = workbook.add_format( + dict(border, align='center')) + self.format_tcell_right = workbook.add_format( + dict(border, align='right')) + self.format_tcell_amount_left = workbook.add_format( + dict(border, num_format=num_format, align='left')) + self.format_tcell_amount_center = workbook.add_format( + dict(border, num_format=num_format, align='center')) + self.format_tcell_amount_right = workbook.add_format( + dict(border, num_format=num_format, align='right')) + self.format_tcell_amount_conditional_left = workbook.add_format( + dict(border, num_format=num_format_conditional, align='left')) + self.format_tcell_amount_conditional_center = workbook.add_format( + dict(border, num_format=num_format_conditional, align='center')) + self.format_tcell_amount_conditional_right = workbook.add_format( + dict(border, num_format=num_format_conditional, align='right')) + self.format_tcell_percent_left = workbook.add_format( + dict(border, num_format=pct_format, align='left')) + self.format_tcell_percent_center = workbook.add_format( + dict(border, num_format=pct_format, align='center')) + self.format_tcell_percent_right = workbook.add_format( + dict(border, num_format=pct_format, align='right')) + self.format_tcell_percent_conditional_left = workbook.add_format( + dict(border, num_format=pct_format_conditional, align='left')) + self.format_tcell_percent_conditional_center = workbook.add_format( + dict(border, num_format=pct_format_conditional, align='center')) + self.format_tcell_percent_conditional_right = workbook.add_format( + dict(border, num_format=pct_format_conditional, align='right')) + self.format_tcell_integer_left = workbook.add_format( + dict(border, num_format=int_format, align='left')) + self.format_tcell_integer_center = workbook.add_format( + dict(border, num_format=int_format, align='center')) + self.format_tcell_integer_right = workbook.add_format( + dict(border, num_format=int_format, align='right')) + self.format_tcell_integer_conditional_left = workbook.add_format( + dict(border, num_format=int_format_conditional, align='left')) + self.format_tcell_integer_conditional_center = workbook.add_format( + dict(border, num_format=int_format_conditional, align='center')) + self.format_tcell_integer_conditional_right = workbook.add_format( + dict(border, num_format=int_format_conditional, align='right')) + self.format_tcell_date_left = workbook.add_format( + dict(border, num_format=date_format, align='left')) + self.format_tcell_date_center = workbook.add_format( + dict(border, num_format=date_format, align='center')) + self.format_tcell_date_right = workbook.add_format( + dict(border, num_format=date_format, align='right')) + + self.format_tcell_left_bold = workbook.add_format( + dict(border, align='left', bold=True)) + self.format_tcell_center_bold = workbook.add_format( + dict(border, align='center', bold=True)) + self.format_tcell_right_bold = workbook.add_format( + dict(border, align='right', bold=True)) + self.format_tcell_amount_left_bold = workbook.add_format( + dict(border, num_format=num_format, align='left', bold=True)) + self.format_tcell_amount_center_bold = workbook.add_format( + dict(border, num_format=num_format, align='center', bold=True)) + self.format_tcell_amount_right_bold = workbook.add_format( + dict(border, num_format=num_format, align='right', bold=True)) + self.format_tcell_amount_conditional_left_bold = workbook.\ + add_format(dict(border, num_format=num_format_conditional, + align='left', bold=True)) + self.format_tcell_amount_conditional_center_bold = workbook.\ + add_format(dict(border, num_format=num_format_conditional, + align='center', bold=True)) + self.format_tcell_amount_conditional_right_bold = workbook.\ + add_format(dict(border, num_format=num_format_conditional, + align='right', bold=True)) + self.format_tcell_percent_left_bold = workbook.add_format( + dict(border, num_format=pct_format, align='left', bold=True)) + self.format_tcell_percent_center_bold = workbook.add_format( + dict(border, num_format=pct_format, align='center', bold=True)) + self.format_tcell_percent_right_bold = workbook.add_format( + dict(border, num_format=pct_format, align='right', bold=True)) + self.format_tcell_percent_conditional_left_bold = workbook.\ + add_format(dict(border, num_format=pct_format_conditional, + align='left', bold=True)) + self.format_tcell_percent_conditional_center_bold = workbook.\ + add_format(dict(border, num_format=pct_format_conditional, + align='center', bold=True)) + self.format_tcell_percent_conditional_right_bold = workbook.\ + add_format(dict(border, num_format=pct_format_conditional, + align='right', bold=True)) + self.format_tcell_integer_left_bold = workbook.add_format( + dict(border, num_format=int_format, align='left', bold=True)) + self.format_tcell_integer_center_bold = workbook.add_format( + dict(border, num_format=int_format, align='center', bold=True)) + self.format_tcell_integer_right_bold = workbook.add_format( + dict(border, num_format=int_format, align='right', bold=True)) + self.format_tcell_integer_conditional_left_bold = workbook.\ + add_format(dict(border, num_format=int_format_conditional, + align='left', bold=True)) + self.format_tcell_integer_conditional_center_bold = workbook.\ + add_format(dict(border, num_format=int_format_conditional, + align='center', bold=True)) + self.format_tcell_integer_conditional_right_bold = workbook.\ + add_format(dict(border, num_format=int_format_conditional, + align='right', bold=True)) + self.format_tcell_date_left_bold = workbook.add_format( + dict(border, num_format=date_format, align='left', bold=True)) + self.format_tcell_date_center_bold = workbook.add_format( + dict(border, num_format=date_format, align='center', bold=True)) + self.format_tcell_date_right_bold = workbook.add_format( + dict(border, num_format=date_format, align='right', bold=True)) + + def _set_column_width(self, ws, ws_params): + """ + Set width for all columns included in the 'wanted_list'. + """ + col_specs = ws_params.get('col_specs') + wl = ws_params.get('wanted_list') or [] + for pos, col in enumerate(wl): + if col not in col_specs: + raise UserError(_( + "Programming Error:\n\n" + "The '%s' column is not defined in the worksheet " + "column specifications.") % col) + ws.set_column(pos, pos, col_specs[col]['width']) + + def _write_ws_title(self, ws, row_pos, ws_params, merge_range=False): + """ + Helper function to ensure consistent title formats + troughout all worksheets. + Requires 'title' keyword in ws_params. + """ + title = ws_params.get('title') + if not title: + raise UserError(_( + "Programming Error:\n\n" + "The 'title' parameter is mandatory " + "when calling the '_write_ws_title' method.")) + if merge_range: + wl = ws_params.get('wanted_list') + if wl and len(wl) > 1: + ws.merge_range( + row_pos, 0, row_pos, len(wl) - 1, + title, self.format_ws_title) + else: + ws.write_string(row_pos, 0, title, self.format_ws_title) + return row_pos + 2 + + def _write_line(self, ws, row_pos, ws_params, col_specs_section=None, + render_space=None, default_format=None): + """ + Write a line with all columns included in the 'wanted_list'. + Use the entry defined by the col_specs_section. + An empty cell will be written if no col_specs_section entry + for a column. + """ + col_specs = ws_params.get('col_specs') + wl = ws_params.get('wanted_list') or [] + pos = 0 + for col in wl: + if col not in col_specs: + raise UserError(_( + "Programming Error:\n\n" + "The '%s' column is not defined the worksheet " + "column specifications.") % col) + colspan = col_specs[col].get('colspan') or 1 + cell_spec = col_specs[col].get(col_specs_section) or {} + if not cell_spec: + cell_value = None + cell_type = 'blank' + cell_format = default_format + else: + cell_value = cell_spec.get('value') + if isinstance(cell_value, CodeType): + cell_value = self._eval(cell_value, render_space) + cell_type = cell_spec.get('type') + cell_format = cell_spec.get('format') or default_format + if not cell_type: + # test bool first since isinstance(val, int) returns + # True when type(val) is bool + if isinstance(cell_value, bool): + cell_type = 'boolean' + elif isinstance(cell_value, str): + cell_type = 'string' + elif isinstance(cell_value, (int, float)): + cell_type = 'number' + elif isinstance(cell_value, datetime): + cell_type = 'datetime' + else: + if not cell_value: + cell_type = 'blank' + else: + msg = _( + "%s, _write_line : programming error " + "detected while processing " + "col_specs_section %s, column %s" + ) % (__name__, col_specs_section, col) + if cell_value: + msg += _(", cellvalue %s") + raise UserError(msg) + colspan = cell_spec.get('colspan') or colspan + args_pos = [row_pos, pos] + args_data = [cell_value] + if cell_format: + args_data.append(cell_format) + if colspan > 1: + args_pos += [row_pos, pos + colspan - 1] + args = args_pos + args_data + ws.merge_range(*args) + else: + ws_method = getattr(ws, 'write_%s' % cell_type) + args = args_pos + args_data + ws_method(*args) + pos += colspan + + return row_pos + 1 + + @staticmethod + def _render(code): + return compile(code, '', 'eval') + + @staticmethod + def _eval(val, render_space): + if not render_space: + render_space = {} + if 'datetime' not in render_space: + render_space['datetime'] = datetime + # the use of eval is not a security thread as long as the + # col_specs template is defined in a python module + return eval(val, render_space) # pylint: disable=W0123,W8112 + + @staticmethod + def _rowcol_to_cell(row, col, row_abs=False, col_abs=False): + return xl_rowcol_to_cell(row, col, row_abs=row_abs, col_abs=col_abs) diff --git a/report_xlsx_helper/report/test_partner_report_xlsx.py b/report_xlsx_helper/report/test_partner_report_xlsx.py new file mode 100644 index 00000000..53196771 --- /dev/null +++ b/report_xlsx_helper/report/test_partner_report_xlsx.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2018 Noviat. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class TestPartnerXlsx(models.AbstractModel): + _name = 'report.report_xlsx_helper.test_partner_xlsx' + _inherit = 'report.report_xlsx.abstract' + + def _get_ws_params(self, wb, data, partners): + + partner_template = { + 'name': { + 'header': { + 'value': 'Name', + }, + 'data': { + 'value': self._render("partner.name"), + }, + 'width': 20, + }, + 'number_of_contacts': { + 'header': { + 'value': '# Contacts', + }, + 'data': { + 'value': self._render("len(partner.child_ids)"), + }, + 'width': 10, + }, + 'is_customer': { + 'header': { + 'value': 'Customer', + }, + 'data': { + 'value': self._render("partner.customer"), + }, + 'width': 10, + }, + 'is_customer_formula': { + 'header': { + 'value': 'Customer Y/N ?', + }, + 'data': { + 'type': 'formula', + 'value': self._render("customer_formula"), + }, + 'width': 14, + }, + } + + ws_params = { + 'ws_name': 'Partners', + 'generate_ws_method': '_partner_report', + 'title': 'Partners', + 'wanted_list': [k for k in partner_template], + 'col_specs': partner_template, + } + + return [ws_params] + + def _partner_report(self, workbook, ws, ws_params, data, partners): + + ws.set_portrait() + ws.fit_to_pages(1, 0) + ws.set_header(self.xls_headers['standard']) + ws.set_footer(self.xls_footers['standard']) + + self._set_column_width(ws, ws_params) + + row_pos = 0 + row_pos = self._write_ws_title(ws, row_pos, ws_params) + row_pos = self._write_line( + ws, row_pos, ws_params, col_specs_section='header', + default_format=self.format_theader_yellow_left) + ws.freeze_panes(row_pos, 0) + + wl = ws_params['wanted_list'] + + for partner in partners: + is_customer_pos = 'is_customer' in wl and \ + wl.index('is_customer') + is_customer_cell = self._rowcol_to_cell( + row_pos, is_customer_pos) + customer_formula = 'IF({},"Y", "N")'.format(is_customer_cell) + row_pos = self._write_line( + ws, row_pos, ws_params, col_specs_section='data', + render_space={ + 'partner': partner, + 'customer_formula': customer_formula, + }, + default_format=self.format_tcell_left) diff --git a/report_xlsx_helper/static/description/icon.png b/report_xlsx_helper/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/report_xlsx_helper/tests/__init__.py b/report_xlsx_helper/tests/__init__.py new file mode 100644 index 00000000..e33d6b90 --- /dev/null +++ b/report_xlsx_helper/tests/__init__.py @@ -0,0 +1 @@ +from . import test_report_xlsx_helper diff --git a/report_xlsx_helper/tests/test_report_xlsx_helper.py b/report_xlsx_helper/tests/test_report_xlsx_helper.py new file mode 100644 index 00000000..059c1243 --- /dev/null +++ b/report_xlsx_helper/tests/test_report_xlsx_helper.py @@ -0,0 +1,23 @@ +# Copyright 2009-2018 Noviat. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests.common import TransactionCase + + +class TestReportXlsxHelper(TransactionCase): + + def setUp(self): + super(TestReportXlsxHelper, self).setUp() + p1 = self.env.ref('base.res_partner_1') + p2 = self.env.ref('base.res_partner_2') + self.partners = p1 + p2 + ctx = { + 'report_name': 'report_xlsx_helper.test_partner_xlsx', + 'active_model': 'res.partner', + 'active_ids': self.partners.ids, + } + self.report = self.env['ir.actions.report'].with_context(ctx) + + def test_report_xlsx_helper(self): + report_xls = self.report.render_xlsx(None, None) + self.assertEqual(report_xls[1], 'xlsx') diff --git a/report_xlsx_helper_demo/README.rst b/report_xlsx_helper_demo/README.rst new file mode 100644 index 00000000..5730d9db --- /dev/null +++ b/report_xlsx_helper_demo/README.rst @@ -0,0 +1,58 @@ +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: https://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +================================== +Excel report engine helpers - demo +================================== + +This module demonstrates the capabilities or the report_xlsx_helper module via +a basic example. + +Usage +===== + +Open a partner record and click on the 'Export XLS' button. + +Installation +============ + +There is no specific installation procedure for this module. + +Configuration and Usage +======================= + +.. 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/11.0 + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Luc De Meyer + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit http://odoo-community.org. diff --git a/report_xlsx_helper_demo/__init__.py b/report_xlsx_helper_demo/__init__.py new file mode 100644 index 00000000..bf588bc8 --- /dev/null +++ b/report_xlsx_helper_demo/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import report diff --git a/report_xlsx_helper_demo/__manifest__.py b/report_xlsx_helper_demo/__manifest__.py new file mode 100644 index 00000000..c7c05651 --- /dev/null +++ b/report_xlsx_helper_demo/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2009-2018 Noviat. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Report xlsx helpers - demo', + 'author': 'Noviat,' + 'Odoo Community Association (OCA)', + 'category': 'Reporting', + 'version': '11.0.1.0.0', + 'license': 'AGPL-3', + 'depends': [ + 'report_xlsx_helper', + ], + 'data': [ + 'views/res_partner.xml', + ], + 'installable': True, +} diff --git a/report_xlsx_helper_demo/models/__init__.py b/report_xlsx_helper_demo/models/__init__.py new file mode 100644 index 00000000..91fed54d --- /dev/null +++ b/report_xlsx_helper_demo/models/__init__.py @@ -0,0 +1 @@ +from . import res_partner diff --git a/report_xlsx_helper_demo/models/res_partner.py b/report_xlsx_helper_demo/models/res_partner.py new file mode 100644 index 00000000..14fff263 --- /dev/null +++ b/report_xlsx_helper_demo/models/res_partner.py @@ -0,0 +1,25 @@ +# Copyright 2009-2018 Noviat +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + @api.multi + def export_xls(self): + module = __name__.split('addons.')[1].split('.')[0] + report_name = '{}.partner_export_xlsx'.format(module) + report = { + 'type': 'ir.actions.report', + 'report_type': 'xlsx', + 'report_name': report_name, + # model name will be used if no report_file passed via context + 'context': dict(self.env.context, report_file='partner'), + # report_xlsx doesn't pass the context if the data dict is empty + # cf. report_xlsx\static\src\js\report\qwebactionmanager.js + # TODO: create PR on report_xlsx to fix this + 'data': {'dynamic_report': True}, + } + return report diff --git a/report_xlsx_helper_demo/report/__init__.py b/report_xlsx_helper_demo/report/__init__.py new file mode 100644 index 00000000..af555af1 --- /dev/null +++ b/report_xlsx_helper_demo/report/__init__.py @@ -0,0 +1 @@ +from . import partner_export_xlsx diff --git a/report_xlsx_helper_demo/report/partner_export_xlsx.py b/report_xlsx_helper_demo/report/partner_export_xlsx.py new file mode 100644 index 00000000..3ea0c813 --- /dev/null +++ b/report_xlsx_helper_demo/report/partner_export_xlsx.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# Copyright 2009-2018 Noviat. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class PartnerExportXlsx(models.AbstractModel): + _name = 'report.report_xlsx_helper_demo.partner_export_xlsx' + _inherit = 'report.report_xlsx.abstract' + + def _get_ws_params(self, wb, data, partners): + + partner_template = { + 'name': { + 'header': { + 'value': 'Name', + }, + 'data': { + 'value': self._render("partner.name"), + }, + 'width': 20, + }, + 'number_of_contacts': { + 'header': { + 'value': '# Contacts', + }, + 'data': { + 'value': self._render("len(partner.child_ids)"), + }, + 'width': 10, + }, + 'is_customer': { + 'header': { + 'value': 'Customer', + }, + 'data': { + 'value': self._render("partner.customer"), + }, + 'width': 10, + }, + 'is_customer_formula': { + 'header': { + 'value': 'Customer Y/N ?', + }, + 'data': { + 'type': 'formula', + 'value': self._render("customer_formula"), + }, + 'width': 14, + }, + } + + wanted_list = [ + 'name', 'number_of_contacts', 'is_customer', + 'is_customer_formula'] + ws_params = { + 'ws_name': 'Partners', + 'generate_ws_method': '_partner_report', + 'title': 'Partners', + 'wanted_list': wanted_list, + 'col_specs': partner_template, + } + + return [ws_params] + + def _partner_report(self, workbook, ws, ws_params, data, partners): + + ws.set_portrait() + ws.fit_to_pages(1, 0) + ws.set_header(self.xls_headers['standard']) + ws.set_footer(self.xls_footers['standard']) + + self._set_column_width(ws, ws_params) + + row_pos = 0 + if len(partners) == 1: + ws_params['title'] = partners.name + row_pos = self._write_ws_title(ws, row_pos, ws_params) + row_pos = self._write_line( + ws, row_pos, ws_params, col_specs_section='header', + default_format=self.format_theader_yellow_left) + ws.freeze_panes(row_pos, 0) + + wl = ws_params['wanted_list'] + + for partner in partners: + is_customer_pos = 'is_customer' in wl and \ + wl.index('is_customer') + is_customer_cell = self._rowcol_to_cell( + row_pos, is_customer_pos) + customer_formula = 'IF({},"Y", "N")'.format(is_customer_cell) + row_pos = self._write_line( + ws, row_pos, ws_params, col_specs_section='data', + render_space={ + 'partner': partner, + 'customer_formula': customer_formula, + }, + default_format=self.format_tcell_left) diff --git a/report_xlsx_helper_demo/static/description/icon.png b/report_xlsx_helper_demo/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/report_xlsx_helper_demo/views/res_partner.xml b/report_xlsx_helper_demo/views/res_partner.xml new file mode 100644 index 00000000..63f46199 --- /dev/null +++ b/report_xlsx_helper_demo/views/res_partner.xml @@ -0,0 +1,19 @@ + + + + + res.partner.test_xlsx + res.partner + + +

+ +
+ + + +