diff --git a/l10n_nl_tax_statement_icp/__init__.py b/l10n_nl_tax_statement_icp/__init__.py index 0650744f6..bf588bc8b 100644 --- a/l10n_nl_tax_statement_icp/__init__.py +++ b/l10n_nl_tax_statement_icp/__init__.py @@ -1 +1,2 @@ from . import models +from . import report diff --git a/l10n_nl_tax_statement_icp/__manifest__.py b/l10n_nl_tax_statement_icp/__manifest__.py index 3cb564a88..cdbd81e26 100644 --- a/l10n_nl_tax_statement_icp/__manifest__.py +++ b/l10n_nl_tax_statement_icp/__manifest__.py @@ -8,7 +8,7 @@ "license": "AGPL-3", "author": "Onestein, Odoo Community Association (OCA)", "website": "https://github.com/OCA/l10n-netherlands", - "depends": ["l10n_nl_tax_statement"], + "depends": ["l10n_nl_tax_statement", "report_xlsx"], "data": [ "security/ir.model.access.csv", "views/l10n_nl_vat_statement_view.xml", diff --git a/l10n_nl_tax_statement_icp/models/l10n_nl_vat_statement_icp_line.py b/l10n_nl_tax_statement_icp/models/l10n_nl_vat_statement_icp_line.py index 04e12dde3..bd7f374f4 100644 --- a/l10n_nl_tax_statement_icp/models/l10n_nl_vat_statement_icp_line.py +++ b/l10n_nl_tax_statement_icp/models/l10n_nl_vat_statement_icp_line.py @@ -17,6 +17,7 @@ class VatStatementIcpLine(models.Model): readonly=True, required=True, ) + partner_name = fields.Char(related="partner_id.name") vat = fields.Char( string="VAT", readonly=True, @@ -25,6 +26,7 @@ class VatStatementIcpLine(models.Model): readonly=True, ) currency_id = fields.Many2one("res.currency", readonly=True) + currency_name = fields.Char(related="currency_id.name", string="Currency Name") amount_products = fields.Monetary(readonly=True) format_amount_products = fields.Char(compute="_compute_icp_amount_format") amount_services = fields.Monetary(readonly=True) diff --git a/l10n_nl_tax_statement_icp/report/__init__.py b/l10n_nl_tax_statement_icp/report/__init__.py new file mode 100644 index 000000000..74218661a --- /dev/null +++ b/l10n_nl_tax_statement_icp/report/__init__.py @@ -0,0 +1 @@ +from . import l10n_nl_tax_statement_icp_xlsx diff --git a/l10n_nl_tax_statement_icp/report/l10n_nl_tax_statement_icp_xlsx.py b/l10n_nl_tax_statement_icp/report/l10n_nl_tax_statement_icp_xlsx.py new file mode 100644 index 000000000..dc3b36b6c --- /dev/null +++ b/l10n_nl_tax_statement_icp/report/l10n_nl_tax_statement_icp_xlsx.py @@ -0,0 +1,227 @@ +# Copyright 2023 Onestein () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, models + + +class NLTaxStatementIcpXlsx(models.AbstractModel): + _name = "report.l10n_nl_tax_statement_icp.report_tax_statement_icp_xlsx" + _description = "NL ICP Statement XLSX report" + _inherit = "report.report_xlsx.abstract" + + def generate_xlsx_report(self, workbook, data, objects): + # Initialize common report variables + report_data = { + "workbook": workbook, + "sheet": None, # main sheet which will contains report + "columns": self._get_report_columns(), # columns of the report + "row_pos": None, # row_pos must be incremented at each writing lines + "formats": None, + } + + # One sheet per statement + for obj in objects: + # Initialize per-sheet variables + report_name = obj.name + self._define_formats(obj, workbook, report_data) + report_data["sheet"] = workbook.add_worksheet(report_name) + self._set_column_width(report_data) + + # Fill report + report_data["row_pos"] = 0 + self._write_report_title(report_name, report_data) + self._generate_report_content(obj, report_data) + + def _get_report_filters(self, report): + return [ + [_("Date from"), report.from_date.strftime("%d/%m/%Y")], + [_("Date to"), report.to_date.strftime("%d/%m/%Y")], + ] + + def _set_column_width(self, report_data): + """Set width for all defined columns. + Columns are defined with `_get_report_columns` method. + """ + for position, column in report_data["columns"].items(): + report_data["sheet"].set_column(position, position, column["width"]) + + def _define_formats(self, obj, workbook, report_data): + """Add cell formats to current workbook. + Those formats can be used on all cell. + Available formats are : + * bold + * header_left + * header_right + * row_odd + * row_pair + * row_amount_odd + * row_amount_pair + """ + color_row_odd = "#FFFFFF" + color_row_pair = "#EEEEEE" + color_row_header = "#FFFFCC" + num_format = "#,##0." + "0" * obj.currency_id.decimal_places + + report_data["formats"] = { + "bold": workbook.add_format({"bold": True}), + "bold_amount": workbook.add_format({"bold": True}), + "header_left": workbook.add_format( + { + "bold": True, + "align": "left", + "border": False, + "bg_color": color_row_header, + } + ), + "header_right": workbook.add_format( + { + "bold": True, + "align": "right", + "border": False, + "bg_color": color_row_header, + } + ), + "row_odd": workbook.add_format( + {"border": False, "bg_color": color_row_odd} + ), + "row_pair": workbook.add_format( + {"border": False, "bg_color": color_row_pair} + ), + "row_amount_odd": workbook.add_format( + {"border": False, "bg_color": color_row_odd} + ), + "row_amount_pair": workbook.add_format( + {"border": False, "bg_color": color_row_pair} + ), + } + report_data["formats"]["bold_amount"].set_num_format(num_format) + report_data["formats"]["row_amount_odd"].set_num_format(num_format) + report_data["formats"]["row_amount_pair"].set_num_format(num_format) + + def _get_report_columns(self): + """Define the report columns used to generate report""" + return { + 0: {"header": _("Partner"), "field": "partner_name", "width": 60}, + 1: {"header": _("VAT"), "field": "vat", "width": 50}, + 2: {"header": _("Country Code"), "field": "country_code", "width": 14}, + 3: {"header": _("Currency"), "field": "currency_name", "width": 14}, + 4: {"header": _("Amount Product"), "field": "amount_products", "width": 20}, + 5: {"header": _("Amount Service"), "field": "amount_services", "width": 20}, + } + + def _write_report_title(self, title, report_data): + """Write report title on current line using all defined columns width. + Columns are defined with `_get_report_columns` method. + """ + report_data["sheet"].merge_range( + report_data["row_pos"], + 0, + report_data["row_pos"], + len(report_data["columns"]) - 1, + title, + report_data["formats"]["bold"], + ) + report_data["row_pos"] += 2 + + def _write_filters(self, filters, report_data, sep=" "): + """Write one line per filter, starting on current row""" + for title, value in filters: + report_data["sheet"].write_string( + report_data["row_pos"], + 0, + title + sep + value, + ) + report_data["row_pos"] += 1 + report_data["row_pos"] += 2 + + def format_line_from_obj(self, line, report_data, is_pair_line): + """Write statement line on current row""" + + for col_pos, column in report_data["columns"].items(): + value = line[column["field"]] + + if isinstance(value, float): + + report_format = ( + report_data["formats"]["row_amount_pair"] + if is_pair_line + else report_data["formats"]["row_amount_odd"] + ) + + report_data["sheet"].write_number( + report_data["row_pos"], col_pos, float(value), report_format + ) + else: + + report_format = ( + report_data["formats"]["row_pair"] + if is_pair_line + else report_data["formats"]["row_odd"] + ) + + report_data["sheet"].write_string( + report_data["row_pos"], + col_pos, + value or "", + report_format, + ) + report_data["row_pos"] += 1 + + def _generate_report_content(self, obj, report_data): + """Write statement content""" + # Write filters + filters = self._get_report_filters(obj) + self._write_filters(filters, report_data) + + # Case no lines found + if not obj.icp_line_ids: + return + + # Display array header for ICP lines + self.write_array_header(report_data) + + # Write report lines + is_pair_line = 0 + for line in obj.icp_line_ids: + self.format_line_from_obj(line, report_data, is_pair_line) + is_pair_line = 1 if not is_pair_line else 0 + + # Write totals + report_data["sheet"].write_string( + report_data["row_pos"], + 3, + _("Total amount"), + report_data["formats"]["header_left"], + ) + report_data["sheet"].write_string( + report_data["row_pos"], + 4, + _("(product + service)"), + report_data["formats"]["header_left"], + ) + report_data["sheet"].write_number( + report_data["row_pos"], + 5, + float(obj.icp_total), + report_data["formats"]["bold_amount"], + ) + + def write_array_header(self, report_data): + """Write array header on current line using all defined columns name. + Columns are defined with `_get_report_columns` method. + """ + for col_pos, column in report_data["columns"].items(): + + report_format = ( + report_data["formats"]["header_right"] + if column["field"] in ["amount_products", "amount_services"] + else report_data["formats"]["header_left"] + ) + + report_data["sheet"].write( + report_data["row_pos"], + col_pos, + column["header"], + report_format, + ) + report_data["row_pos"] += 1 diff --git a/l10n_nl_tax_statement_icp/tests/test_l10n_nl_tax_statement_icp.py b/l10n_nl_tax_statement_icp/tests/test_l10n_nl_tax_statement_icp.py index 00cafba0e..0da2977b7 100644 --- a/l10n_nl_tax_statement_icp/tests/test_l10n_nl_tax_statement_icp.py +++ b/l10n_nl_tax_statement_icp/tests/test_l10n_nl_tax_statement_icp.py @@ -128,3 +128,15 @@ def test_07_icp_invoice_outside_europe(self): with self.assertRaises(ValidationError): self.statement_with_icp.post() + + def test_08_action_xls(self): + """Generate XLS report from action""" + report = "l10n_nl_tax_statement_icp.action_report_tax_statement_icp_xls_export" + self.report_action = self.env.ref(report) + self.assertEqual(self.report_action.report_type, "xlsx") + model = self.env["report.%s" % self.report_action["report_name"]].with_context( + active_model="l10n.nl.vat.statement" + ) + res = model.create_xlsx_report(self.statement_1.ids, data=None) + self.assertTrue(res[0]) + self.assertEqual(res[1], "xlsx") diff --git a/l10n_nl_tax_statement_icp/views/report_tax_statement.xml b/l10n_nl_tax_statement_icp/views/report_tax_statement.xml index 264eab8c5..43173e4e5 100644 --- a/l10n_nl_tax_statement_icp/views/report_tax_statement.xml +++ b/l10n_nl_tax_statement_icp/views/report_tax_statement.xml @@ -22,4 +22,21 @@ ref="l10n_nl_tax_statement.paperformat_nl_tax_statement" /> + + + NL ICP Statement XLS export + l10n.nl.vat.statement + xlsx + l10n_nl_tax_statement_icp.report_tax_statement_icp_xlsx + l10n_nl_tax_statement_icp.report_tax_statement_icp_xlsx + + report +