diff --git a/account_analytic_report/README.rst b/account_analytic_report/README.rst new file mode 100644 index 000000000000..23508571912a --- /dev/null +++ b/account_analytic_report/README.rst @@ -0,0 +1,129 @@ +======================== +Account Analytic Reports +======================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:e3b2f8d263dd282038c6d240451ddf65612a4d8dfbf754af136900aa97285230 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--financial--reporting-lightgray.png?logo=github + :target: https://github.com/OCA/account-financial-reporting/tree/17.0/account_analytic_report + :alt: OCA/account-financial-reporting +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/account-financial-reporting-17-0/account-financial-reporting-17-0-account_analytic_report + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/account-financial-reporting&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module introduces an analytic report that provides an intuitive way +to view and analyze analytic balances. It simplifies the process, +offering enhanced insights and making it easier to leverage this +information effectively. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Using this module is straightforward. Follow these steps: + +- | **Navigate to the Report**: + | Go to **Invoicing** -> **Reporting** -> **Analytic Trial Balance**. + +- | **Customize the Report with Filters**: + | Adjust the report using the available options: + + - | **Group by Analytic Account**: + | Groups the results by analytic accounts instead of financial + accounts. + + - | **Show Hierarchy and Limit Hierarchy Level**: + | Displays the amounts split by the hierarchy levels of financial + accounts. + + - | **Filter Accounts**: + | When used independently (without grouping by analytic accounts + or showing hierarchy), the results will be split by both + financial accounts. + | **Example**: Filtering by accounts *Test 1* and *Test 2*: + + .. code:: text + + | Initial Balance | Test 1 | Test 2 | Ending Balance + 400000 | 0 | $3600 | $2400 | $6000 + + - | **Show Months** (Excel export only): + | Enabled when filtering accounts without grouping by analytic + accounts or showing hierarchy. It generates a separate sheet in + the Excel file for each filtered account, detailing the amounts + by month within the selected date range. + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* APSL-Nagarro + +Contributors +------------ + +- `APSL-Nagarro `__: + + - Bernat Obrador + - Miquel Alzanillas + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +.. |maintainer-BernatObrador| image:: https://github.com/BernatObrador.png?size=40px + :target: https://github.com/BernatObrador + :alt: BernatObrador +.. |maintainer-miquelalzanillas| image:: https://github.com/miquelalzanillas.png?size=40px + :target: https://github.com/miquelalzanillas + :alt: miquelalzanillas + +Current `maintainers `__: + +|maintainer-BernatObrador| |maintainer-miquelalzanillas| + +This module is part of the `OCA/account-financial-reporting `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_analytic_report/__init__.py b/account_analytic_report/__init__.py new file mode 100644 index 000000000000..c4e388b714a9 --- /dev/null +++ b/account_analytic_report/__init__.py @@ -0,0 +1,2 @@ +from . import report +from . import wizard diff --git a/account_analytic_report/__manifest__.py b/account_analytic_report/__manifest__.py new file mode 100644 index 000000000000..a0b96320fad0 --- /dev/null +++ b/account_analytic_report/__manifest__.py @@ -0,0 +1,30 @@ +# Copyright 2024 (APSL - Nagarro) Bernat Obrador +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Account Analytic Reports", + "version": "17.0.1.0.0", + "summary": "OCA Analytic Reports", + "author": "APSL-Nagarro, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/account-financial-reporting", + "category": "Account", + "depends": ["analytic", "account_financial_report"], + "maintainers": ["BernatObrador", "miquelalzanillas"], + "data": [ + "security/ir.model.access.csv", + "wizard/trial_balance_analytic_wizard_view.xml", + "menuitems.xml", + "reports.xml", + "report/templates/trial_balance_analytic.xml", + "views/report_trial_balance_analytic.xml", + "views/account_analytic_line.xml", + ], + "assets": { + "web.assets_backend": [ + "account_analytic_report/static/src/js/*", + ], + }, + "application": False, + "installable": True, + "auto_install": False, + "license": "AGPL-3", +} diff --git a/account_analytic_report/i18n/es.po b/account_analytic_report/i18n/es.po new file mode 100644 index 000000000000..e46cb22cdb43 --- /dev/null +++ b/account_analytic_report/i18n/es.po @@ -0,0 +1,410 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_analytic_report +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-11-14 07:15+0000\n" +"PO-Revision-Date: 2024-11-14 07:15+0000\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: account_analytic_report +#: model:ir.model.fields,help:account_analytic_report.field_ac_trial_balance_report_wizard__show_months +msgid "" +"\n" +" This option works only when exporting to Excel. It will create a separate sheet\n" +" for each selected analytic account, displaying all financial accounts with a\n" +" balance.\n" +" For each account, it shows the monthly balance within the selected date range.\n" +" " +msgstr "" +"Esta opción funciona solo al exportar a Excel. Creará una hoja separada para cada cuenta analítica seleccionada, mostrando todas las cuentas financieras con un saldo.\n" +"Para cada cuenta, muestra el saldo mensual dentro del rango de fechas seleccionado." + +#. module: account_analytic_report +#. odoo-python +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#: model_terms:ir.ui.view,arch_db:account_analytic_report.report_trial_balance_lines_header +#, python-format +msgid "Account" +msgstr "Cuenta" + +#. module: account_analytic_report +#. odoo-python +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#: model_terms:ir.ui.view,arch_db:account_analytic_report.report_trial_balance_filters +#, python-format +msgid "Account at 0 filter" +msgstr "Filtro de cuentas a 0" + +#. module: account_analytic_report +#: model_terms:ir.ui.view,arch_db:account_analytic_report.report_trial_balance_filters +msgid "All accounts" +msgstr "Todas las cuentas" + +#. module: account_analytic_report +#. odoo-python +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#: model:ir.actions.act_window,name:account_analytic_report.action_analytic_trial_balance_wizard +#: model:ir.ui.menu,name:account_analytic_report.menu_analytic_trial_balance +#, python-format +msgid "Analytic Trial Balance" +msgstr "Balance Analítico" + +#. module: account_analytic_report +#: model_terms:ir.ui.view,arch_db:account_analytic_report.report_trial_balance_base +msgid "Analytic Trial Balance -" +msgstr "Balance Analítico -" + +#. module: account_analytic_report +#: model:ir.model,name:account_analytic_report.model_ac_trial_balance_report_wizard +msgid "Analytic Trial Balance Report Wizard" +msgstr "Asistente de balance analítico" + +#. module: account_analytic_report +#: model_terms:ir.ui.view,arch_db:account_analytic_report.analytic_trial_balance_wizard +msgid "Cancel" +msgstr "Cancelar" + +#. module: account_analytic_report +#. odoo-python +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#: model_terms:ir.ui.view,arch_db:account_analytic_report.report_trial_balance_lines_header +#, python-format +msgid "Code" +msgstr "Código" + +#. module: account_analytic_report +#: model:ir.model.fields,field_description:account_analytic_report.field_ac_trial_balance_report_wizard__company_id +msgid "Company" +msgstr "Compañía" + +#. module: account_analytic_report +#: model:ir.model.fields,field_description:account_analytic_report.field_ac_trial_balance_report_wizard__create_uid +msgid "Created by" +msgstr "" + +#. module: account_analytic_report +#: model:ir.model.fields,field_description:account_analytic_report.field_ac_trial_balance_report_wizard__create_date +msgid "Created on" +msgstr "" + +#. module: account_analytic_report +#: model:ir.model.fields,field_description:account_analytic_report.field_ac_trial_balance_report_wizard__date_from +msgid "Date From" +msgstr "Desde" + +#. module: account_analytic_report +#: model:ir.model.fields,field_description:account_analytic_report.field_ac_trial_balance_report_wizard__date_to +msgid "Date To" +msgstr "Hasta" + +#. module: account_analytic_report +#: model:ir.model.fields,field_description:account_analytic_report.field_ac_trial_balance_report_wizard__date_range_id +msgid "Date range" +msgstr "Periodo" + +#. module: account_analytic_report +#. odoo-python +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#: model_terms:ir.ui.view,arch_db:account_analytic_report.report_trial_balance_filters +#, python-format +msgid "Date range filter" +msgstr "Filtro de fechas" + +#. module: account_analytic_report +#: model:ir.model.fields,field_description:account_analytic_report.field_ac_trial_balance_report_wizard__display_name +msgid "Display Name" +msgstr "" + +#. module: account_analytic_report +#. odoo-python +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#: model_terms:ir.ui.view,arch_db:account_analytic_report.report_trial_balance_lines_header +#, python-format +msgid "Ending balance" +msgstr "Saldo Final" + +#. module: account_analytic_report +#: model_terms:ir.ui.view,arch_db:account_analytic_report.analytic_trial_balance_wizard +msgid "Export PDF" +msgstr "Exportar PDF" + +#. module: account_analytic_report +#: model_terms:ir.ui.view,arch_db:account_analytic_report.analytic_trial_balance_wizard +msgid "Export XLSX" +msgstr "Exportar XLSX" + +#. module: account_analytic_report +#: model:ir.model.fields,field_description:account_analytic_report.field_ac_trial_balance_report_wizard__account_ids +msgid "Filter accounts" +msgstr "Filtrar cuentas" + +#. module: account_analytic_report +#: model_terms:ir.ui.view,arch_db:account_analytic_report.report_trial_balance_filters +msgid "From:" +msgstr "Desde:" + +#. module: account_analytic_report +#. odoo-python +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#, python-format +msgid "From: %(date_from)s To: %(date_to)s" +msgstr "" + +#. module: account_analytic_report +#: model:ir.model.fields,field_description:account_analytic_report.field_ac_trial_balance_report_wizard__fy_start_date +msgid "Fy Start Date" +msgstr "Fecha inicio ejercicio fiscal" + +#. module: account_analytic_report +#: model:ir.model.fields,field_description:account_analytic_report.field_ac_trial_balance_report_wizard__group_by_analytic_account +#: model_terms:ir.ui.view,arch_db:account_analytic_report.report_trial_balance_filters +msgid "Group by Analytic Account" +msgstr "Agrupar por cuenta analítica" + +#. module: account_analytic_report +#. odoo-python +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#, python-format +msgid "Grouped by analytic account" +msgstr "" + +#. module: account_analytic_report +#. odoo-python +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#: model_terms:ir.ui.view,arch_db:account_analytic_report.report_trial_balance_filters +#, python-format +msgid "Hide" +msgstr "Ocultar" + +#. module: account_analytic_report +#: model:ir.model.fields,field_description:account_analytic_report.field_ac_trial_balance_report_wizard__hide_account_at_0 +msgid "Hide accounts at 0" +msgstr "Ocultar cuentas a 0" + +#. module: account_analytic_report +#: model:ir.model.fields,field_description:account_analytic_report.field_ac_trial_balance_report_wizard__hierarchy_level +msgid "Hierarchy Level" +msgstr "Nivel de Jerarquía\n" + +#. module: account_analytic_report +#: model:ir.model.fields,help:account_analytic_report.field_ac_trial_balance_report_wizard__hierarchy_level +msgid "Hierarchy levels to show" +msgstr "Nivel de Jerarquía a mostrar\n" + +#. module: account_analytic_report +#: model:ir.model.fields,field_description:account_analytic_report.field_ac_trial_balance_report_wizard__id +msgid "ID" +msgstr "" + +#. module: account_analytic_report +#. odoo-python +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#: model_terms:ir.ui.view,arch_db:account_analytic_report.report_trial_balance_lines_header +#, python-format +msgid "Initial balance" +msgstr "Saldo Inicial" + +#. module: account_analytic_report +#: model:ir.model.fields,field_description:account_analytic_report.field_ac_trial_balance_report_wizard__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: account_analytic_report +#: model:ir.model.fields,field_description:account_analytic_report.field_ac_trial_balance_report_wizard__write_date +msgid "Last Updated on" +msgstr "" + +#. module: account_analytic_report +#: model_terms:ir.ui.view,arch_db:account_analytic_report.report_trial_balance_filters +msgid "Level" +msgstr "Nivel" + +#. module: account_analytic_report +#. odoo-python +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#, python-format +msgid "Level %s" +msgstr "" + +#. module: account_analytic_report +#: model:ir.model.fields,field_description:account_analytic_report.field_ac_trial_balance_report_wizard__limit_hierarchy_level +msgid "Limit Hierarchy Level" +msgstr "Limitar niveles de jerarquía" + +#. module: account_analytic_report +#. odoo-python +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#: model_terms:ir.ui.view,arch_db:account_analytic_report.report_trial_balance_filters +#, python-format +msgid "Limit hierarchy levels" +msgstr "Limitar niveles de jerarquía" + +#. module: account_analytic_report +#: model:ir.model.fields,help:account_analytic_report.field_ac_trial_balance_report_wizard__limit_hierarchy_level +msgid "Limits hierarchy level" +msgstr "Límites de niveles de jerarquía" + +#. module: account_analytic_report +#. odoo-python +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#: model_terms:ir.ui.view,arch_db:account_analytic_report.report_trial_balance_filters +#, python-format +msgid "No" +msgstr "" + +#. module: account_analytic_report +#. odoo-python +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#: model_terms:ir.ui.view,arch_db:account_analytic_report.report_trial_balance_filters +#, python-format +msgid "No limit" +msgstr "Sin Nivel" + +#. module: account_analytic_report +#: model:ir.ui.menu,name:account_analytic_report.menu_oca_analytic_reports +msgid "OCA Analytic reports" +msgstr "Reportes Analíticos OCA" + +#. module: account_analytic_report +#. odoo-python +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#: model_terms:ir.ui.view,arch_db:account_analytic_report.report_trial_balance_lines_header +#, python-format +msgid "Period balance" +msgstr "Saldo Periodo" + +#. module: account_analytic_report +#: model:ir.model.fields,field_description:account_analytic_report.field_ac_trial_balance_report_wizard__plan_id +msgid "Plan" +msgstr "Plan Contable" + +#. module: account_analytic_report +#. odoo-python +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#, python-format +msgid "Selected Plan" +msgstr "" + +#. module: account_analytic_report +#. odoo-python +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#: model_terms:ir.ui.view,arch_db:account_analytic_report.report_trial_balance_filters +#, python-format +msgid "Show" +msgstr "Mostrar" + +#. module: account_analytic_report +#: model:ir.model.fields,field_description:account_analytic_report.field_ac_trial_balance_report_wizard__show_hierarchy +msgid "Show Hierarchy" +msgstr "Mostrar jerarquía" + +#. module: account_analytic_report +#: model:ir.model.fields,field_description:account_analytic_report.field_ac_trial_balance_report_wizard__show_months +msgid "Show Months" +msgstr "Mostrar meses" + +#. module: account_analytic_report +#: model:ir.model.fields,help:account_analytic_report.field_ac_trial_balance_report_wizard__show_hierarchy +msgid "Shows hierarchy of the financial accounts" +msgstr "Mostrar jerarquía de cuentas financieras" + +#. module: account_analytic_report +#: model_terms:ir.ui.view,arch_db:account_analytic_report.report_trial_balance_filters +msgid "Target Plan" +msgstr "Plan analítico" + +#. module: account_analytic_report +#: model_terms:ir.ui.view,arch_db:account_analytic_report.report_trial_balance_filters +msgid "Target accounts filter" +msgstr "Filtro de cuentas" + +#. module: account_analytic_report +#. odoo-python +#: code:addons/account_analytic_report/wizard/trial_balance_analytic_wizard_view.py:0 +#, python-format +msgid "" +"The Company in the Trial Balance Report Wizard and in Date Range must be the" +" same." +msgstr "" + +#. module: account_analytic_report +#. odoo-python +#: code:addons/account_analytic_report/wizard/trial_balance_analytic_wizard_view.py:0 +#, python-format +msgid "The hierarchy level to filter on must be greater than 0." +msgstr "" + +#. module: account_analytic_report +#: model_terms:ir.ui.view,arch_db:account_analytic_report.report_trial_balance_filters +msgid "To" +msgstr "Hasta:" + +#. module: account_analytic_report +#. odoo-python +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#, python-format +msgid "Total" +msgstr "" + +#. module: account_analytic_report +#: model:ir.actions.report,name:account_analytic_report.action_report_analytic_trial_balance_html +#: model:ir.actions.report,name:account_analytic_report.action_report_analytic_trial_balance_qweb +msgid "Trial Analytic Balance" +msgstr "Balance Analítico" + +#. module: account_analytic_report +#: model:ir.model,name:account_analytic_report.model_report_account_analytic_report_trial_balance_analytic +msgid "Trial Balance Analytic Report" +msgstr "Informe de balance analítico" + +#. module: account_analytic_report +#: model:ir.actions.report,name:account_analytic_report.action_report_analytic_trial_balance_xlsx +msgid "Trial Balance XLSX" +msgstr "Balance Analítico XLSX" + +#. module: account_analytic_report +#: model:ir.model,name:account_analytic_report.model_report_a_f_r_report_trial_balance_analytic_xlsx +msgid "Trial Balance XLSX Report" +msgstr "Informe XLSX de balance analítico" + +#. module: account_analytic_report +#: model_terms:ir.ui.view,arch_db:account_analytic_report.analytic_trial_balance_wizard +msgid "View" +msgstr "Ver" + +#. module: account_analytic_report +#: model:ir.model.fields,help:account_analytic_report.field_ac_trial_balance_report_wizard__hide_account_at_0 +msgid "" +"When this option is enabled, the trial balance will not display accounts " +"that have initial balance = debit = credit = end balance = 0" +msgstr "" +"Cuando esta opción está habilitada, el balance de comprobación no mostrará " +"cuentas cuyo saldo inicial = débito = crédito = saldo final = 0." + +#. module: account_analytic_report +#. odoo-python +#: code:addons/account_analytic_report/report/trial_balance_analytic_xlsx.py:0 +#: model_terms:ir.ui.view,arch_db:account_analytic_report.report_trial_balance_filters +#, python-format +msgid "Yes" +msgstr "Si" + +#. module: account_analytic_report +#: model_terms:ir.ui.view,arch_db:account_analytic_report.analytic_trial_balance_wizard +msgid "or" +msgstr "o" diff --git a/account_analytic_report/menuitems.xml b/account_analytic_report/menuitems.xml new file mode 100644 index 000000000000..64f8e6a2f939 --- /dev/null +++ b/account_analytic_report/menuitems.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/account_analytic_report/pyproject.toml b/account_analytic_report/pyproject.toml new file mode 100644 index 000000000000..4231d0cccb3d --- /dev/null +++ b/account_analytic_report/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/account_analytic_report/readme/CONTRIBUTORS.md b/account_analytic_report/readme/CONTRIBUTORS.md new file mode 100644 index 000000000000..4ad78d6af5ff --- /dev/null +++ b/account_analytic_report/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- [APSL-Nagarro](https://apsl.tech): + - Bernat Obrador + - Miquel Alzanillas diff --git a/account_analytic_report/readme/DESCRIPTION.md b/account_analytic_report/readme/DESCRIPTION.md new file mode 100644 index 000000000000..7f76bb60ec1f --- /dev/null +++ b/account_analytic_report/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This module introduces an analytic report that provides an intuitive way to view and analyze analytic balances. It simplifies the process, offering enhanced insights and making it easier to leverage this information effectively. \ No newline at end of file diff --git a/account_analytic_report/readme/USAGE.md b/account_analytic_report/readme/USAGE.md new file mode 100644 index 000000000000..36d21cd620be --- /dev/null +++ b/account_analytic_report/readme/USAGE.md @@ -0,0 +1,25 @@ +Using this module is straightforward. Follow these steps: + +* **Navigate to the Report**: + Go to **Invoicing** -> **Reporting** -> **Analytic Trial Balance**. + +* **Customize the Report with Filters**: + Adjust the report using the available options: + + * **Group by Analytic Account**: + Groups the results by analytic accounts instead of financial accounts. + + * **Show Hierarchy and Limit Hierarchy Level**: + Displays the amounts split by the hierarchy levels of financial accounts. + + * **Filter Accounts**: + When used independently (without grouping by analytic accounts or showing hierarchy), the results will be split by both financial accounts. + **Example**: Filtering by accounts *Test 1* and *Test 2*: + + ```text + | Initial Balance | Test 1 | Test 2 | Ending Balance + 400000 | 0 | $3600 | $2400 | $6000 + ``` + + * **Show Months** (Excel export only): + Enabled when filtering accounts without grouping by analytic accounts or showing hierarchy. It generates a separate sheet in the Excel file for each filtered account, detailing the amounts by month within the selected date range. diff --git a/account_analytic_report/report/__init__.py b/account_analytic_report/report/__init__.py new file mode 100644 index 000000000000..db771ac683a0 --- /dev/null +++ b/account_analytic_report/report/__init__.py @@ -0,0 +1,2 @@ +from . import trial_balance_analytic +from . import trial_balance_analytic_xlsx diff --git a/account_analytic_report/report/templates/trial_balance_analytic.xml b/account_analytic_report/report/templates/trial_balance_analytic.xml new file mode 100644 index 000000000000..d83721f155e8 --- /dev/null +++ b/account_analytic_report/report/templates/trial_balance_analytic.xml @@ -0,0 +1,492 @@ + + + + + + + + + + + + diff --git a/account_analytic_report/report/trial_balance_analytic.py b/account_analytic_report/report/trial_balance_analytic.py new file mode 100644 index 000000000000..b707251cc960 --- /dev/null +++ b/account_analytic_report/report/trial_balance_analytic.py @@ -0,0 +1,872 @@ +# Copyright 2024 (APSL - Nagarro) Bernat Obrador +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo import api, models +from odoo.tools.float_utils import float_is_zero + + +class TrialBalanceAnalyticReport(models.AbstractModel): + _name = "report.account_analytic_report.trial_balance_analytic" + _description = "Trial Balance Analytic Report" + _inherit = "report.account_financial_report.abstract_report" + + def _get_accounts_data(self, accounts_ids, group_by_field): + if group_by_field == "general_account_id": + accounts = self.env["account.account"].search([("id", "in", accounts_ids)]) + else: + accounts = self.env["account.analytic.account"].search( + [("id", "in", accounts_ids)] + ) + accounts_data = {} + for account in accounts: + accounts_data.update( + { + account.id: { + "id": account.id, + "name": account.name, + "code": account.code if account.code else account.name, + } + } + ) + return accounts_data + + def _get_base_domain( + self, account_ids, company_id, account_id_field, plan_id, group_by_field + ): + accounts_domain = [ + ("company_id", "=", company_id), + ("root_plan_id", "=", plan_id), + ] + if account_ids: + accounts_domain += [("id", "in", account_ids)] + accounts = self.env["account.analytic.account"].search(accounts_domain) + + domain = [ + (account_id_field, "in", accounts.ids), + (account_id_field, "!=", False), + (group_by_field, "!=", False), + ] + if company_id: + domain += [("company_id", "=", company_id)] + return domain + + def _get_initial_balances_bs_ml_domain(self, domain, date_from, fy_start_date): + bs_ml_domain = domain + [ + ("date", "<", date_from), + ("date", ">=", fy_start_date), + ] + return bs_ml_domain + + @api.model + def _get_period_ml_domain( + self, + domain, + date_to, + date_from, + ): + ml_domain = domain + [ + ("date", ">=", date_from), + ("date", "<=", date_to), + ] + return ml_domain + + @api.model + def _compute_account_amount( + self, + total_amount, + tb_initial_acc, + tb_period_acc, + group_by_field, + account_id_field=None, + account_ids=None, + ): + """ + Prepares the total amount dict with inital balance, period balance and + ending balance. + If account_ids is not null and we are not grouping by analytic account + it will split the ammount in the analytic account and financial account + """ + for tb in tb_period_acc: + if tb[group_by_field]: + self._prepare_amounts( + tb, group_by_field, total_amount, account_id_field, account_ids + ) + for tb in tb_initial_acc: + id_field = group_by_field if account_ids else "account_id" + acc_id = tb[id_field] + if acc_id not in total_amount.keys(): + total_amount[acc_id] = self._prepare_total_amount(tb, account_ids) + else: + total_amount[acc_id]["initial_balance"] = tb["amount"] + total_amount[acc_id]["ending_balance"] += tb["amount"] + return total_amount + + def _prepare_amounts( + self, tb, group_by_field, total_amount, account_id_field, account_ids=None + ): + if account_ids: + acc_id = tb[group_by_field][0] + if acc_id not in total_amount.keys(): + total_amount[acc_id] = self._prepare_total_amount(tb, account_ids) + total_amount[acc_id][tb[account_id_field][0]] = tb["amount"] + total_amount[acc_id]["initial_balance"] = 0.0 + else: + total_amount[acc_id][tb[account_id_field][0]] = tb["amount"] + total_amount[acc_id]["ending_balance"] += tb["amount"] + total_amount[acc_id]["initial_balance"] = 0.0 + else: + acc_id = tb[group_by_field][0] + total_amount[acc_id] = self._prepare_total_amount(tb) + total_amount[acc_id]["amount"] = tb["amount"] + total_amount[acc_id]["initial_balance"] = 0.0 + + @api.model + def _prepare_total_amount(self, tb, account_ids=None): + res = { + "amount": 0.0, + "initial_balance": tb["amount"], + "ending_balance": tb["amount"], + } + if account_ids: + for account in account_ids: + res[account] = 0.0 + + return res + + def _remove_accounts_at_cero(self, total_amount, company): + def is_removable(d): + rounding = company.currency_id.rounding + return float_is_zero( + d["initial_balance"], precision_rounding=rounding + ) and float_is_zero(d["ending_balance"], precision_rounding=rounding) + + accounts_to_remove = [] + for acc_id, ta_data in total_amount.items(): + if is_removable(ta_data): + accounts_to_remove.append(acc_id) + for account_id in accounts_to_remove: + del total_amount[account_id] + + def _get_hierarchy_groups(self, group_ids, groups_data): + for group_id in group_ids: + parent_id = groups_data[group_id]["parent_id"] + while parent_id: + if parent_id not in groups_data.keys(): + group = self.env["account.group"].browse(parent_id) + groups_data[group.id] = { + "id": group.id, + "code": group.code_prefix_start, + "name": group.name, + "parent_id": group.parent_id.id, + "parent_path": group.parent_path, + "complete_code": group.complete_code, + "account_ids": group.compute_account_ids.ids, + "type": "group_type", + "initial_balance": 0, + "balance": 0, + "ending_balance": 0, + } + acc_keys = ["balance"] + acc_keys += ["initial_balance", "ending_balance"] + for acc_key in acc_keys: + groups_data[parent_id][acc_key] += groups_data[group_id][acc_key] + parent_id = groups_data[parent_id]["parent_id"] + return groups_data + + def _get_groups_data(self, accounts_data, total_amount): + accounts_ids = list(accounts_data.keys()) + accounts = self.env["account.account"].browse(accounts_ids) + account_group_relation = {} + for account in accounts: + accounts_data[account.id]["complete_code"] = ( + account.group_id.complete_code + " / " + account.code + if account.group_id.id + else "" + ) + if account.group_id.id: + if account.group_id.id not in account_group_relation.keys(): + account_group_relation.update({account.group_id.id: [account.id]}) + else: + account_group_relation[account.group_id.id].append(account.id) + groups = self.env["account.group"].browse(account_group_relation.keys()) + groups_data = {} + for group in groups: + groups_data.update( + { + group.id: { + "id": group.id, + "code": group.code_prefix_start, + "name": group.name, + "parent_id": group.parent_id.id, + "parent_path": group.parent_path, + "type": "group_type", + "complete_code": group.complete_code, + "account_ids": group.compute_account_ids.ids, + "initial_balance": 0.0, + "balance": 0.0, + "ending_balance": 0.0, + } + } + ) + for group_id in account_group_relation.keys(): + for account_id in account_group_relation[group_id]: + groups_data[group_id]["initial_balance"] += total_amount[account_id][ + "initial_balance" + ] + groups_data[group_id]["balance"] += total_amount[account_id]["amount"] + groups_data[group_id]["ending_balance"] += total_amount[account_id][ + "ending_balance" + ] + group_ids = list(groups_data.keys()) + groups_data = self._get_hierarchy_groups( + group_ids, + groups_data, + ) + return groups_data + + def _get_computed_groups_data(self, accounts_data, total_amount): + groups = self.env["account.group"].search([("id", "!=", False)]) + groups_data = {} + for group in groups: + len_group_code = len(group.code_prefix_start) + groups_data.update( + { + group.id: { + "id": group.id, + "code": group.code_prefix_start, + "name": group.name, + "parent_id": group.parent_id.id, + "parent_path": group.parent_path, + "type": "group_type", + "complete_code": group.complete_code, + "account_ids": group.compute_account_ids.ids, + "initial_balance": 0.0, + "balance": 0.0, + "ending_balance": 0.0, + } + } + ) + for account in accounts_data.values(): + if group.code_prefix_start == account["code"][:len_group_code]: + acc_id = account["id"] + group_id = group.id + groups_data[group_id]["initial_balance"] += total_amount[acc_id][ + "initial_balance" + ] + groups_data[group_id]["balance"] += total_amount[acc_id]["balance"] + groups_data[group_id]["ending_balance"] += total_amount[acc_id][ + "ending_balance" + ] + return groups_data + + def _hide_accounts_at_0(self, company_id, total_amount): + company = self.env["res.company"].browse(company_id) + self._remove_accounts_at_cero(total_amount, company) + + def _get_tb_initial_acc_bs( + self, domain, date_from, fy_start_date, fields, group_by, lazy=True + ): + initial_domain_bs = self._get_initial_balances_bs_ml_domain( + domain, + date_from, + fy_start_date, + ) + return self.env["account.analytic.line"].read_group( + domain=initial_domain_bs, + fields=fields, + groupby=group_by, + lazy=lazy, + ) + + def _get_tb_period_acc( + self, domain, date_to, date_from, fields, group_by, lazy=True + ): + period_domain = self._get_period_ml_domain( + domain, + date_to, + date_from, + ) + return self.env["account.analytic.line"].read_group( + domain=period_domain, fields=fields, groupby=group_by, lazy=lazy + ) + + def _get_account_codes(self, account_ids): + analytic_accounts = self.env["account.analytic.account"].search( + [("id", "in", account_ids)] + ) + account_codes = [ + account.code if account.code else account.name + for account in sorted(analytic_accounts, key=lambda account: account.id) + ] + codes_string = ", ".join(account_codes) + return codes_string + + def _clean_account_codes(self, account_codes): + return ( + [code.strip() for code in account_codes.split(",")] + if account_codes + else None + ) + + def _update_accounts_data( + self, + accounts_data, + total_amount, + total_amounts, + include_both_accounts=False, + account_ids=None, + ): + for account_id in accounts_data.keys(): + accounts_data[account_id].update( + { + "initial_balance": total_amount[account_id]["initial_balance"], + "ending_balance": total_amount[account_id]["ending_balance"], + "type": "account_type", + "code": accounts_data[account_id]["code"], + } + ) + total_amounts["total_initial_balance"] += total_amount[account_id][ + "initial_balance" + ] + total_amounts["total_ending_balance"] += total_amount[account_id][ + "ending_balance" + ] + # If the report requires both account details, add a nested + # structure within each account. So now we can have the amount + # by the analytic account and the financial account + if include_both_accounts: + accounts_data[account_id]["accounts"] = {} + for account in account_ids: + accounts_data[account_id]["accounts"][account] = total_amount[ + account_id + ][account] + if account not in total_amounts["total_period_balance"]: + total_amounts["total_period_balance"][account] = 0 + total_amounts["total_period_balance"][account] += total_amount[ + account_id + ][account] + else: + accounts_data[account_id].update( + {"balance": total_amount[account_id]["amount"]} + ) + total_amounts["total_period_balance"] += total_amount[account_id][ + "amount" + ] + + def _get_trial_balance(self, accounts_data, total_amount, show_hierarchy): + if show_hierarchy: + groups_data = self._get_groups_data(accounts_data, total_amount) + trial_balance = list(groups_data.values()) + list(accounts_data.values()) + trial_balance = sorted(trial_balance, key=lambda k: k["complete_code"]) + for trial in trial_balance: + trial["level"] = trial["complete_code"].count("/") + else: + trial_balance = list(accounts_data.values()) + return trial_balance + + def _get_total_amounts_dict(self, include_both_accounts): + return { + "total_initial_balance": 0, + "total_period_balance": {} if include_both_accounts else 0, + "total_ending_balance": 0, + } + + def _get_archived_account_ids(self, company_id): + return ( + self.env["account.analytic.account"] + .search([("company_id", "=", company_id), ("active", "=", False)]) + .ids + ) + + @api.model + def _get_data_splited_by_accounts( + self, + account_ids, + company_id, + date_to, + date_from, + fy_start_date, + plan_field, + plan_id, + ): + """ + This function gives the report grouped by financial account and + analytic account spliting the ammount by the 2 accounts + """ + domain = self._get_base_domain( + account_ids, company_id, plan_field, plan_id, "general_account_id" + ) + tb_initial_acc_bs = self._get_tb_initial_acc_bs( + domain=domain, + date_from=date_from, + fy_start_date=fy_start_date, + fields=[plan_field, "general_account_id", "amount"], + group_by=["general_account_id", plan_field], + lazy=False, + ) + tb_initial_acc = [] + for line in tb_initial_acc_bs: + tb_initial_acc.append( + { + "general_account_id": line["general_account_id"][0], + plan_field: line[plan_field][0], + "amount": line["amount"], + } + ) + + tb_initial_acc = [p for p in tb_initial_acc if p["amount"] != 0] + + tb_period_acc = self._get_tb_period_acc( + domain=domain, + date_to=date_to, + date_from=date_from, + fields=[plan_field, "general_account_id", "amount"], + group_by=["general_account_id", plan_field], + lazy=False, + ) + + total_amount = {} + total_amount = self._compute_account_amount( + total_amount, + tb_initial_acc, + tb_period_acc, + "general_account_id", + plan_field, + account_ids, + ) + + self._hide_accounts_at_0(company_id, total_amount) + + accounts_ids = list(total_amount.keys()) + accounts_data = self._get_accounts_data(accounts_ids, "general_account_id") + + return total_amount, accounts_data + + @api.model + def _get_data( + self, + account_ids, + company_id, + date_to, + date_from, + fy_start_date, + plan_field, + plan_id, + group_by_analytic_account, + ): + """ + This function gives the report grouped by financial account + """ + group_by_field = ( + plan_field if group_by_analytic_account else "general_account_id" + ) + + domain = self._get_base_domain( + account_ids, company_id, plan_field, plan_id, group_by_field + ) + + accounts_domain = [("company_id", "=", company_id)] + if account_ids: + accounts_domain += [("id", "in", account_ids)] + + if group_by_field == "general_account_id": + accounts = self.env["account.account"].search(accounts_domain) + else: + accounts = self.env["account.analytic.account"].search(accounts_domain) + tb_initial_acc = [] + + for account in accounts: + tb_initial_acc.append({"account_id": account.id, "amount": 0.0}) + + tb_initial_acc_bs = self._get_tb_initial_acc_bs( + domain=domain, + date_from=date_from, + fy_start_date=fy_start_date, + fields=[plan_field, "general_account_id", "amount"], + group_by=[group_by_field], + ) + for account_rg in tb_initial_acc_bs: + element = list( + filter( + lambda acc_dict: acc_dict["account_id"] + == account_rg[group_by_field][0], + tb_initial_acc, + ) + ) + if element: + element[0]["amount"] += account_rg["amount"] + + tb_initial_acc = [p for p in tb_initial_acc if p["amount"] != 0] + + tb_period_acc = self._get_tb_period_acc( + domain=domain, + date_to=date_to, + date_from=date_from, + fields=[plan_field, "general_account_id", "amount"], + group_by=[group_by_field], + ) + + total_amount = {} + total_amount = self._compute_account_amount( + total_amount, tb_initial_acc, tb_period_acc, group_by_field + ) + + self._hide_accounts_at_0(company_id, total_amount) + + accounts_ids = list(total_amount.keys()) + accounts_data = self._get_accounts_data(accounts_ids, group_by_field) + + return total_amount, accounts_data + + def _get_base_total_by_acc_type_select(self, include_both_accounts, plan_field): + if include_both_accounts: + return f""" + SELECT aa.account_type, aal.{plan_field}, sum(amount) + FROM account_analytic_line AS aal + INNER JOIN account_account AS aa ON aa.id = aal.general_account_id + """ + return """ + SELECT aa.account_type, sum(amount) + FROM account_analytic_line AS aal + INNER JOIN account_account AS aa ON aa.id = aal.general_account_id + """ + + def _get_base_total_by_acc_type_where(self, company_id, account_ids, plan_field): + account_ids_where = ( + f"AND aal.{plan_field} in ({','.join(map(str, account_ids))})" + if account_ids + else "" + ) + archives_account_ids = self._get_archived_account_ids(company_id) + acrhived_account_ids_where = ( + f"AND aal.{plan_field} not in ({','.join(map(str, archives_account_ids))})" + if archives_account_ids + else "" + ) + + return f""" + WHERE aal.company_id = {company_id} + {account_ids_where} + {acrhived_account_ids_where} + AND aal.{plan_field} is not null + """ + + def _get_base_total_acc_type_group_by(self, include_both_accounts, plan_field): + if include_both_accounts: + return f""" + GROUP BY aa.account_type, aal.{plan_field} + """ + return """ + GROUP BY aa.account_type + """ + + def _get_account_type_mapping(self): + return dict( + self.env["account.account"].fields_get(allfields=["account_type"])[ + "account_type" + ]["selection"] + ) + + def _map_accounts_type_by_name( + self, results, account_type_mapping, balance_type, include_both_accounts + ): + result_dict = {} + + # If balance type its period we need to make a specific key for the account_ids + # To have the amount splitted by financial account and analytic account + if balance_type == "total_period_balance" and include_both_accounts: + key_format = "{}|{}" + else: + key_format = "{}" + + for result in results: + if len(result) == 3: + account_type, account_id, total = result + elif len(result) == 2: + account_type, total = result + account_id = None + else: + continue + + account_type_name = account_type_mapping.get(account_type, account_type) + + if include_both_accounts and account_id is not None: + key = key_format.format(account_type_name, account_id) + else: + key = key_format.format(account_type_name) + + if key in result_dict: + result_dict[key] += total + else: + result_dict[key] = total + + return result_dict + + def _get_total_initial_by_acc_type( + self, + base_select, + base_where, + base_group_by, + date_from, + fy_start_date, + ): + query = f""" + {base_select} + {base_where} + AND aal.date < %s + AND aal.date >= %s + {base_group_by} + """ + params = [date_from, fy_start_date] + self.env.cr.execute(query, params) + + return self.env.cr.fetchall() + + def _get_total_period_by_acc_type( + self, + base_select, + base_where, + base_group_by, + date_from, + date_to, + ): + query = f""" + {base_select} + {base_where} + AND aal.date >= %s + AND aal.date <= %s + {base_group_by} + """ + params = [date_from, date_to] + self.env.cr.execute(query, params) + + return self.env.cr.fetchall() + + def _update_balance_by_account_type( + self, balance_type, totals_by_acc_type, totals_dict + ): + for acc_type in totals_by_acc_type: + totals_dict[acc_type][balance_type] = totals_by_acc_type[acc_type] + totals_dict[acc_type]["total_ending_balance"] += totals_by_acc_type[ + acc_type + ] + + def _get_totals_by_acc_type( + self, + company_id, + account_ids, + date_from, + date_to, + plan_field, + group_by_analytic_account, + include_both_accounts, + fy_start_date, + ): + """ + This function calculates and returns the totals + for each account type, providing greater analytical + precision for the report. + Period balance will change if the report includes + the analytic accounts too. + ex: + Inital Balance | Period Balance | Ending Balance + Income: 1.250€ 250€ 1.500€ + Epxense: -500€ -125€ -625€ + """ + account_type_mapping = self._get_account_type_mapping() + base_select = self._get_base_total_by_acc_type_select( + include_both_accounts, plan_field + ) + base_where = self._get_base_total_by_acc_type_where( + company_id, account_ids, plan_field + ) + base_group_by = self._get_base_total_acc_type_group_by( + include_both_accounts, plan_field + ) + + account_types_total_dict = { + account_type_name: self._get_total_amounts_dict(include_both_accounts) + for _account_type, account_type_name in account_type_mapping.items() + } + for _account_type, balances in account_types_total_dict.items(): + for account_id in account_ids: + # Si tenemos que incluir los dos tipos de cuentas entonces + # Debemos crear un subapartado por cada cuenta + if include_both_accounts: + if account_id not in balances["total_period_balance"]: + balances["total_period_balance"][account_id] = 0 + else: + balances["total_period_balance"] = 0 + + total_initial_by_acc_type = self._get_total_initial_by_acc_type( + base_select, base_where, base_group_by, date_from, fy_start_date + ) + + total_period_by_acc_type = self._get_total_period_by_acc_type( + base_select, + base_where, + base_group_by, + date_from, + date_to, + ) + + total_initial_by_acc_type = self._map_accounts_type_by_name( + total_initial_by_acc_type, + account_type_mapping, + "total_initial_balance", + include_both_accounts, + ) + total_period_by_acc_type = self._map_accounts_type_by_name( + total_period_by_acc_type, + account_type_mapping, + "total_period_balance", + include_both_accounts, + ) + + if include_both_accounts: + for key, value in total_period_by_acc_type.items(): + account_type, account_id = key.split("|") + account_id = int(account_id) + if ( + account_id + not in account_types_total_dict[account_type][ + "total_period_balance" + ].keys() + ): + account_types_total_dict[account_type]["total_period_balance"][ + account_id + ] = 0 + account_types_total_dict[account_type]["total_period_balance"][ + account_id + ] += value + account_types_total_dict[account_type]["total_ending_balance"] += value + else: + self._update_balance_by_account_type( + "total_period_balance", + total_period_by_acc_type, + account_types_total_dict, + ) + + self._update_balance_by_account_type( + "total_initial_balance", total_initial_by_acc_type, account_types_total_dict + ) + + # Deletes account types with 0 amounts + filtered_account_types_total_dict = { + account_type_name: balances + for account_type_name, balances in account_types_total_dict.items() + if balances["total_ending_balance"] + } + + return filtered_account_types_total_dict + + def _get_report_values(self, docids, data): + wizard_id = data["wizard_id"] + company = self.env["res.company"].browse(data["company_id"]) + + account_codes = self._get_account_codes(data["account_ids"]) + account_code_list = self._clean_account_codes(account_codes) + + if ( + data["account_ids"] + and not data["group_by_analytic_account"] + and not data["show_hierarchy"] + ): + total_amount, accounts_data = self._get_data_splited_by_accounts( + data["account_ids"], + data["company_id"], + data["date_to"], + data["date_from"], + data["fy_start_date"], + data["plan_field"], + data["plan_id"], + ) + include_both_accounts = True + else: + total_amount, accounts_data = self._get_data( + data["account_ids"], + data["company_id"], + data["date_to"], + data["date_from"], + data["fy_start_date"], + data["plan_field"], + data["plan_id"], + data["group_by_analytic_account"], + ) + include_both_accounts = False + + totals_by_acc_type = self._get_totals_by_acc_type( + data["company_id"], + data["account_ids"], + data["date_from"], + data["date_to"], + data["plan_field"], + data["group_by_analytic_account"], + include_both_accounts, + data["fy_start_date"], + ) + + total_amounts = self._get_total_amounts_dict(include_both_accounts) + self._update_accounts_data( + accounts_data, + total_amount, + total_amounts, + include_both_accounts=include_both_accounts, + account_ids=data["account_ids"], + ) + trial_balance = self._get_trial_balance( + accounts_data, total_amount, data["show_hierarchy"] + ) + + return self._prepare_report_values( + wizard_id, + company, + data, + trial_balance, + total_amount, + accounts_data, + account_codes, + account_code_list, + total_amounts, + totals_by_acc_type, + ) + + def _prepare_report_values( + self, + wizard_id, + company, + data, + trial_balance, + total_amount, + accounts_data, + account_codes, + account_code_list, + total_amounts, + totals_by_acc_type, + ): + return { + "doc_ids": [wizard_id], + "doc_model": "ac.trial.balance.report.wizard", + "docs": self.env["ac.trial.balance.report.wizard"].browse(wizard_id), + "company_name": company.display_name, + "currency_name": company.currency_id.name, + "date_from": data["date_from"], + "date_to": data["date_to"], + "trial_balance": trial_balance, + "total_amount": total_amount, + "accounts_data": accounts_data, + "plan_name": data["plan_name"], + "plan_field": data["plan_field"], + "group_by_analytic_account": data["group_by_analytic_account"], + "show_hierarchy": data["show_hierarchy"], + "limit_hierarchy_level": data["limit_hierarchy_level"], + "show_hierarchy_level": data["hierarchy_level"], + "account_codes": account_codes, + "account_code_list": account_code_list, + "account_ids": data["account_ids"], + "show_months": data["show_months"], + "total_amounts": total_amounts, + "archived_accounts": tuple(self._get_archived_account_ids(company.id)), + "totals_by_acc_type": totals_by_acc_type, + } diff --git a/account_analytic_report/report/trial_balance_analytic_xlsx.py b/account_analytic_report/report/trial_balance_analytic_xlsx.py new file mode 100644 index 000000000000..390b4e35b73f --- /dev/null +++ b/account_analytic_report/report/trial_balance_analytic_xlsx.py @@ -0,0 +1,642 @@ +# Copyright 2024 (APSL - Nagarro) Bernat Obrador +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import datetime + +from dateutil.relativedelta import relativedelta + +from odoo import _, models + + +class TrialBalanceXslx(models.AbstractModel): + _name = "report.a_f_r.report_trial_balance_analytic_xlsx" + _description = "Trial Balance XLSX Report" + _inherit = "report.account_financial_report.abstract_report_xlsx" + + def _get_report_name(self, report, data=False): + company_id = data.get("company_id", False) + account_code = data.get("account_code", False) + report_name = _("Analytic Trial Balance") + if company_id: + company = self.env["res.company"].browse(company_id) + suffix = f" - {company.name} - {company.currency_id.name}" + report_name = report_name + suffix + if account_code: + report_name += f" [{account_code}]" + return report_name + + def _define_formats(self, workbook, report_data): + currency_id = self.env["res.company"]._default_currency_id() + col_format_totals = { + "bold": True, + "bg_color": "#90cf00", + "border": True, + } + + col_format_totals_by_acc_type = { + "bold": True, + "bg_color": "#D9EBD3", + "border": True, + } + report_data["formats"]["format_total"] = workbook.add_format(col_format_totals) + report_data["formats"]["format_amount_total"] = workbook.add_format( + col_format_totals + ) + report_data["formats"]["format_amount_total"].set_num_format( + "#,##0." + "0" * currency_id.decimal_places + ) + + report_data["formats"]["format_acc_type_total"] = workbook.add_format( + col_format_totals_by_acc_type + ) + report_data["formats"]["format_acc_type_amount_total"] = workbook.add_format( + col_format_totals_by_acc_type + ) + report_data["formats"]["format_acc_type_amount_total"].set_num_format( + "#,##0." + "0" * currency_id.decimal_places + ) + + return super()._define_formats(workbook, report_data) + + def _is_report_with_include_both_accounts(self, report): + return report.account_ids and not report.group_by_analytic_account + + def _get_report_columns(self, report): + if self._is_report_with_include_both_accounts(report): + codes = self.env["account.analytic.account"].search( + [("id", "in", report.account_ids.ids)] + ) + res = { + 0: {"header": _("Code"), "field": "code", "width": 15}, + 1: {"header": _("Account"), "field": "name", "width": 70}, + 2: { + "header": _("Initial balance"), + "field": "initial_balance", + "type": "amount", + "width": 14, + }, + } + for i, account in enumerate(codes): + res[i + 3] = { + "header": account.code, + "id": account.id, + "field": "accounts", + "type": "amount", + "width": 14, + } + + res[len(res)] = { + "header": _("Ending balance"), + "field": "ending_balance", + "type": "amount", + "width": 14, + } + else: + res = { + 0: {"header": _("Code"), "field": "code", "width": 10}, + 1: {"header": _("Account"), "field": "name", "width": 70}, + 2: { + "header": _("Initial balance"), + "field": "initial_balance", + "type": "amount", + "width": 14, + }, + 3: { + "header": _("Period balance"), + "field": "balance", + "type": "amount", + "width": 14, + }, + 4: { + "header": _("Ending balance"), + "field": "ending_balance", + "type": "amount", + "width": 14, + }, + } + return res + + def _get_report_filters(self, report): + report_filters = [ + [ + _("Date range filter"), + _("From: %(date_from)s To: %(date_to)s") + % ({"date_from": report.date_from, "date_to": report.date_to}), + ], + [_("Selected Plan"), report.plan_id.name], + [ + _("Grouped by analytic account"), + _("Yes") if report.group_by_analytic_account else _("No"), + ], + ] + + if report.account_ids: + account_codes = self.env[ + "report.account_analytic_report.trial_balance_analytic" + ]._get_account_codes(report.account_ids.ids) + report_filters.append( + [ + _("Accounts Filter"), + account_codes, + ] + ) + + report_filters.append( + [ + _("Limit hierarchy levels"), + ( + _("Level %s") % (report.hierarchy_level) + if report.limit_hierarchy_level + else _("No limit") + ), + ] + ) + + return report_filters + + def _get_col_count_filter_name(self): + return 2 + + def _get_col_count_filter_value(self): + return 3 + + def _write_trial_analytic(self, report_values, report_data): + for balance in report_values["trial_balance"]: + if ( + report_values["show_hierarchy"] + and report_values["limit_hierarchy_level"] + ): + if report_values["hierarchy_level"] > balance["level"]: + self.write_line_from_dict(balance, report_data) + else: + self.write_line_from_dict(balance, report_data) + + def _generate_report_content(self, workbook, report, data, report_data): + report_values = self._get_values_from_report(report, data) + report_data["account_code_list"] = report_values["account_code_list"] + report_data["group_by_analytic_account"] = report_values[ + "group_by_analytic_account" + ] + report_data["total_amounts"] = report_values["total_amounts"] + self.write_array_header(report_data) + + self._write_trial_analytic(report_values, report_data) + + total_rows_by_account_type = self._prepare_total_rows_by_account_type( + report_values, report + ) + + for row in total_rows_by_account_type: + self._write_line_with_format( + report_data, + row, + report_data["formats"]["format_acc_type_total"], + report_data["formats"]["format_acc_type_amount_total"], + ) + + total_row = self._prepare_total_row(report_values, report) + self._write_line_with_format( + report_data, + total_row, + report_data["formats"]["format_total"], + report_data["formats"]["format_amount_total"], + ) + + if report_values["show_months"]: + self.create_page_by_anlytic_accounts( + workbook, report, report_data, report_values + ) + + def _prepare_total_row(self, report_values, report): + total_row = { + "name": _("Total"), + "initial_balance": report_values["total_amounts"]["total_initial_balance"], + "ending_balance": report_values["total_amounts"]["total_ending_balance"], + } + if self._is_report_with_include_both_accounts(report): + total_row["accounts"] = {} + codes = self.env["account.analytic.account"].search( + [("id", "in", report.account_ids.ids)] + ) + for account in codes: + total_row["accounts"].update( + { + account.id: report_values["total_amounts"][ + "total_period_balance" + ][account.id] + } + ) + else: + total_row["balance"] = report_values["total_amounts"][ + "total_period_balance" + ] + + return total_row + + def _prepare_total_rows_by_account_type(self, report_values, report): + total_rows = [] + for acc_type, balances in report_values["totals_by_acc_type"].items(): + total_row = { + "name": acc_type, + "initial_balance": balances["total_initial_balance"], + "ending_balance": balances["total_ending_balance"], + } + if self._is_report_with_include_both_accounts(report): + total_row["accounts"] = {} + codes = self.env["account.analytic.account"].search( + [("id", "in", report.account_ids.ids)] + ) + for account in codes: + if account.id in balances["total_period_balance"].keys(): + total_row["accounts"].update( + {account.id: balances["total_period_balance"][account.id]} + ) + else: + total_row["balance"] = balances["total_period_balance"] + total_rows.append(total_row) + return total_rows + + def _get_values_from_report(self, report, data): + res_data = self.env[ + "report.account_analytic_report.trial_balance_analytic" + ]._get_report_values(report, data) + return { + "show_hierarchy": res_data["show_hierarchy"], + "hierarchy_level": res_data["show_hierarchy_level"], + "limit_hierarchy_level": res_data["limit_hierarchy_level"], + "account_code_list": res_data["account_code_list"], + "show_months": res_data["show_months"], + "group_by_analytic_account": res_data["group_by_analytic_account"], + "trial_balance": res_data["trial_balance"], + "plan_field": res_data["plan_field"], + "total_amounts": res_data["total_amounts"], + "totals_by_acc_type": res_data["totals_by_acc_type"], + "account_ids": res_data["account_ids"], + } + + def write_line_from_dict(self, line_dict, report_data): + if not ( + report_data["account_code_list"] + and not report_data["group_by_analytic_account"] + ): + return super().write_line_from_dict(line_dict, report_data) + else: + for col_pos, column in report_data["columns"].items(): + value = line_dict.get(column["field"], False) + cell_type = column.get("type", "string") + if cell_type == "string": + if line_dict.get("type", "") == "group_type": + report_data["sheet"].write_string( + report_data["row_pos"], + col_pos, + value or "", + report_data["formats"]["format_bold"], + ) + else: + if ( + not isinstance(value, str) + and not isinstance(value, bool) + and not isinstance(value, int) + ): + value = value and value.strftime("%d/%m/%Y") + report_data["sheet"].write_string( + report_data["row_pos"], col_pos, value or "" + ) + elif cell_type == "amount": + if ( + line_dict.get("account_group_id", False) + and line_dict["account_group_id"] + ): + cell_format = report_data["formats"]["format_amount_bold"] + else: + cell_format = report_data["formats"]["format_amount"] + if column["field"] == "accounts": + value_to_write = value[column["id"]] + report_data["sheet"].write_number( + report_data["row_pos"], + col_pos, + float(value_to_write), + cell_format, + ) + else: + report_data["sheet"].write_number( + report_data["row_pos"], col_pos, float(value), cell_format + ) + elif cell_type == "amount_currency": + if line_dict.get("currency_name", False): + format_amt = self._get_currency_amt_format_dict( + line_dict, report_data + ) + report_data["sheet"].write_number( + report_data["row_pos"], col_pos, float(value), format_amt + ) + elif cell_type == "currency_name": + report_data["sheet"].write_string( + report_data["row_pos"], + col_pos, + value or "", + report_data["formats"]["format_right"], + ) + else: + self.write_non_standard_column(cell_type, col_pos, value) + report_data["row_pos"] += 1 + + def _write_line_with_format(self, report_data, row, str_format, amount_format): + for col_pos, column in report_data["columns"].items(): + value = row.get(column["field"], False) + cell_type = column.get("type", "string") + + if cell_type == "amount": + value = value if value else 0 + cell_format = amount_format + + if column["field"] == "accounts": + value = value[column["id"]] + report_data["sheet"].write_number( + report_data["row_pos"], col_pos, float(value), cell_format + ) + elif cell_type == "string": + value = value if value else "" + report_data["sheet"].write_string( + report_data["row_pos"], col_pos, str(value), str_format + ) + report_data["row_pos"] += 1 + + def _prepare_data_for_page(self, report): + date_from = report.date_from.strftime("%Y-%m-%d") + date_to = report.date_to.strftime("%Y-%m-%d") + account_id_field = report.plan_id._column_name() + company_id = report.company_id.id + filters = self._get_report_filters(report) + + return { + "date_from": date_from, + "date_to": date_to, + "account_id_field": account_id_field, + "company_id": company_id, + "filters": filters, + } + + def _create_page_for_account( + self, + workbook, + company_id, + report_data, + account, + filters, + date_from, + date_to, + report_values, + ): + """ + Adds a new worksheet for each account using its code as the sheet name. + """ + sheet = workbook.add_worksheet(account.code) + report_data["sheet"] = sheet + report_data["row_pos"] = 0 + filters[4][1] = account.code + + self._write_report_title( + self._get_report_name( + report_data, {"company_id": company_id, "account_code": account.code} + ), + report_data, + ) + self._write_filters(filters, report_data) + + return sheet + + def _get_report_columns_by_month(self, date_from, date_to, account): + res = { + 0: {"header": _("Code"), "field": "code", "width": 15}, + 1: {"header": _("Account"), "field": "name", "width": 50}, + } + + date_from = datetime.strptime(date_from, "%Y-%m-%d") + date_to = datetime.strptime(date_to, "%Y-%m-%d") + + current_date = date_from + + # Loop through each month between date_from and date_to + while current_date <= date_to: + month_year = current_date.strftime("%m-%Y") + + # Add a new column for this month + res[len(res)] = { + "header": month_year, + "field": f"{current_date.month}-{current_date.year}", + "type": "amount", + "width": 14, + } + + # Move to the next month + current_date += relativedelta(months=1) + + res[len(res)] = { + "header": _("Total"), + "field": "total", + "type": "amount", + "width": 14, + } + return res + + def _get_months_query( + self, company_id, account_id_field, account, date_from, date_to, report_values + ): + return f""" + SELECT "account_analytic_line"."general_account_id", + date_trunc('month', + "account_analytic_line"."date"::timestamp)::date,COUNT(*), + SUM("account_analytic_line"."amount") + FROM "account_analytic_line" + LEFT JOIN "account_account" AS "account_analytic_line__general_account_id" + ON ("account_analytic_line"."general_account_id" = + "account_analytic_line__general_account_id"."id") + LEFT JOIN "res_company" AS + "account_analytic_line__general_account_id__company_id" + ON ("account_analytic_line__general_account_id"."company_id" = + "account_analytic_line__general_account_id__company_id"."id") + WHERE ( + ("account_analytic_line".{account_id_field} = {account.id}) + AND ("account_analytic_line"."company_id" = {company_id}) + AND ("account_analytic_line"."date" >= '{date_from}') + AND ("account_analytic_line"."date" <= '{date_to}') + ) + GROUP BY "account_analytic_line"."general_account_id", + date_trunc('month', "account_analytic_line"."date"::timestamp)::date, + "account_analytic_line__general_account_id"."code", + "account_analytic_line__general_account_id__company_id"."sequence", + "account_analytic_line__general_account_id__company_id"."name" + ORDER BY "account_analytic_line__general_account_id"."code", + "account_analytic_line__general_account_id__company_id"."sequence", + "account_analytic_line__general_account_id__company_id"."name", + date_trunc('month', "account_analytic_line"."date"::timestamp)::date ASC + """ + + def _get_total_acc_type_by_month_query( + self, company_id, account_id_field, account, date_from, date_to + ): + return f""" + SELECT + aa.account_type, + aal.{account_id_field}, + date_trunc('month', aal.date::timestamp)::date AS month, + SUM(amount) AS total_amount + FROM + account_analytic_line AS aal + INNER JOIN + account_account AS aa ON aa.id = aal.general_account_id + WHERE + aal.company_id = {company_id} + AND aal.{account_id_field} IS NOT NULL + AND aal.date >= '{date_from}' + AND aal.date <= '{date_to}' + AND aal.{account_id_field} = {account.id} + GROUP BY + aa.account_type, + aal.{account_id_field}, + date_trunc('month', aal.date::timestamp)::date + ORDER BY + month ASC; + """ + + def _get_total_acc_type_by_month( + self, company_id, account_id_field, account, date_from, date_to + ): + self.env.cr.execute( + self._get_total_acc_type_by_month_query( + company_id, account_id_field, account, date_from, date_to + ) + ) + total_acc_type_by_months = self.env.cr.fetchall() + + # Maps the accounts with his redebale name + account_type_mapping = self.env[ + "report.account_analytic_report.trial_balance_analytic" + ]._get_account_type_mapping() + + for i, acc_type in enumerate(total_acc_type_by_months): + total_acc_type_by_months[i] = ( + account_type_mapping[acc_type[0]], + *acc_type[1:], + ) + + return total_acc_type_by_months + + def _get_total_acc_type_by_months_rows(self, total_acc_type_by_months): + total_acc_type_by_months_rows = {} + for total_acc_type in total_acc_type_by_months: + account_type_name = total_acc_type[0] + month_year = f"{total_acc_type[2].month}-{total_acc_type[2].year}" + amount = total_acc_type[3] + + if account_type_name not in total_acc_type_by_months_rows: + total_acc_type_by_months_rows[account_type_name] = { + "name": account_type_name, + "total": 0, + } + + total_acc_type_by_months_rows[account_type_name][month_year] = amount + total_acc_type_by_months_rows[account_type_name]["total"] += amount + + return total_acc_type_by_months_rows + + def _get_amounts_and_total_by_analytic_account(self, amounts_data_by_month): + amounts_by_month = {} + total_row = {"code": _("Total"), "total": 0} + for amount_data in amounts_data_by_month: + account_account = self.env["account.account"].browse(amount_data[0]) + key = f"{amount_data[1].month}-{amount_data[1].year}" + amount = amount_data[3] + + total_row[key] = total_row.get(key, 0) + amount + total_row["total"] += amount + + if account_account.id not in amounts_by_month: + amounts_by_month[account_account.id] = { + "code": account_account.code, + "name": account_account.name, + "total": 0, + } + + amounts_by_month[account_account.id][key] = amount + amounts_by_month[account_account.id]["total"] += amount + return amounts_by_month, total_row + + def _write_amount_by_month(self, amounts_by_month, report_data): + for amount_by_month in amounts_by_month.values(): + if isinstance(amount_by_month, dict): + self.write_line_from_dict(amount_by_month, report_data) + + def _write_totals_by_acc_type(self, total_acc_type_by_months, report_data): + total_acc_type_month_row = self._get_total_acc_type_by_months_rows( + total_acc_type_by_months + ) + # Writes total by account type + for row in total_acc_type_month_row.values(): + self._write_line_with_format( + report_data, + row, + report_data["formats"]["format_acc_type_total"], + report_data["formats"]["format_acc_type_amount_total"], + ) + + def _write_total_row(self, total_row, report_data): + # Writes total row + self._write_line_with_format( + report_data, + total_row, + report_data["formats"]["format_total"], + report_data["formats"]["format_amount_total"], + ) + + def create_page_by_anlytic_accounts( + self, workbook, report, report_data, report_values + ): + report_data_values = self._prepare_data_for_page(report) + date_from = report_data_values["date_from"] + date_to = report_data_values["date_to"] + account_id_field = report_data_values["account_id_field"] + filters = report_data_values["filters"] + company_id = report_data_values["company_id"] + for account in report.account_ids: + self._create_page_for_account( + workbook, + company_id, + report_data, + account, + filters, + date_from, + date_to, + report_values, + ) + + query = self._get_months_query( + company_id, account_id_field, account, date_from, date_to, report_values + ) + + self.env.cr.execute(query) + + amounts_data_by_month = self.env.cr.fetchall() + + report_data["columns"] = self._get_report_columns_by_month( + date_from, date_to, account + ) + + self.write_array_header(report_data) + self._set_column_width(report_data) + + ( + amounts_by_month, + total_row, + ) = self._get_amounts_and_total_by_analytic_account(amounts_data_by_month) + amounts_by_month.update({"account_id": account.id}) + self._write_amount_by_month(amounts_by_month, report_data) + + total_acc_type_by_months = self._get_total_acc_type_by_month( + company_id, account_id_field, account, date_from, date_to + ) + + self._write_totals_by_acc_type(total_acc_type_by_months, report_data) + + self._write_total_row(total_row, report_data) diff --git a/account_analytic_report/reports.xml b/account_analytic_report/reports.xml new file mode 100644 index 000000000000..df50cda93d60 --- /dev/null +++ b/account_analytic_report/reports.xml @@ -0,0 +1,47 @@ + + + + + Trial Analytic Balance + ac.trial.balance.report.wizard + qweb-pdf + account_analytic_report.trial_balance_analytic + account_analytic_report.trial_balance_analytic + + + + Trial Analytic Balance + ac.trial.balance.report.wizard + qweb-html + account_analytic_report.trial_balance_analytic + account_analytic_report.trial_balance_analytic + + + Trial Balance XLSX + ac.trial.balance.report.wizard + ir.actions.report + a_f_r.report_trial_balance_analytic_xlsx + xlsx + report_trial_balance_analytic + + diff --git a/account_analytic_report/security/ir.model.access.csv b/account_analytic_report/security/ir.model.access.csv new file mode 100644 index 000000000000..e16db37c5347 --- /dev/null +++ b/account_analytic_report/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_trial_balance_analytic_report_wizard,access_trial_balance_analytic_report_wizard,model_ac_trial_balance_report_wizard,base.group_user,1,1,1,1 diff --git a/account_analytic_report/static/description/icon.png b/account_analytic_report/static/description/icon.png new file mode 100644 index 000000000000..0a917e513df1 Binary files /dev/null and b/account_analytic_report/static/description/icon.png differ diff --git a/account_analytic_report/static/description/index.html b/account_analytic_report/static/description/index.html new file mode 100644 index 000000000000..45f2b3c7a97f --- /dev/null +++ b/account_analytic_report/static/description/index.html @@ -0,0 +1,483 @@ + + + + + +Account Analytic Reports + + + +
+

Account Analytic Reports

+ + +

Beta License: AGPL-3 OCA/account-financial-reporting Translate me on Weblate Try me on Runboat

+

This module introduces an analytic report that provides an intuitive way +to view and analyze analytic balances. It simplifies the process, +offering enhanced insights and making it easier to leverage this +information effectively.

+

Table of contents

+ +
+

Usage

+

Using this module is straightforward. Follow these steps:

+
    +
  • +
    Navigate to the Report:
    +
    Go to Invoicing -> Reporting -> Analytic Trial Balance.
    +
    +
  • +
  • +
    Customize the Report with Filters:
    +
    Adjust the report using the available options:
    +
    +
      +
    • +
      Group by Analytic Account:
      +
      Groups the results by analytic accounts instead of financial +accounts.
      +
      +
    • +
    • +
      Show Hierarchy and Limit Hierarchy Level:
      +
      Displays the amounts split by the hierarchy levels of financial +accounts.
      +
      +
    • +
    • +
      Filter Accounts:
      +
      When used independently (without grouping by analytic accounts +or showing hierarchy), the results will be split by both +financial accounts.
      +
      Example: Filtering by accounts Test 1 and Test 2:
      +
      +
      +        | Initial Balance | Test 1   | Test 2   | Ending Balance
      +400000  |        0        | $3600    | $2400    |     $6000
      +
      +
    • +
    • +
      Show Months (Excel export only):
      +
      Enabled when filtering accounts without grouping by analytic +accounts or showing hierarchy. It generates a separate sheet in +the Excel file for each filtered account, detailing the amounts +by month within the selected date range.
      +
      +
    • +
    +
  • +
+
+
+

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 to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • APSL-Nagarro
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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.

+

Current maintainers:

+

BernatObrador miquelalzanillas

+

This module is part of the OCA/account-financial-reporting project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/account_analytic_report/tests/__init__.py b/account_analytic_report/tests/__init__.py new file mode 100644 index 000000000000..d2dd961ed878 --- /dev/null +++ b/account_analytic_report/tests/__init__.py @@ -0,0 +1 @@ +from . import test_trial_analytic_balance diff --git a/account_analytic_report/tests/test_trial_analytic_balance.py b/account_analytic_report/tests/test_trial_analytic_balance.py new file mode 100644 index 000000000000..c1bac8594c6e --- /dev/null +++ b/account_analytic_report/tests/test_trial_analytic_balance.py @@ -0,0 +1,374 @@ +# Copyright 2024 (APSL - Nagarro) Bernat Obrador +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + +from odoo.tests import tagged +from odoo.tests.common import TransactionCase + + +@tagged("post_install", "-at_install") +class TestTrialAnalyticBalanceReport(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.account_type_map = cls.env[ + "report.account_analytic_report.trial_balance_analytic" + ]._get_account_type_mapping() + + cls.analytic_plan_1 = cls.env["account.analytic.plan"].create( + { + "name": "Plan 1", + } + ) + account_group = cls.env["account.group"] + cls.group5 = account_group.create({"code_prefix_start": "5", "name": "Group 5"}) + cls.group4 = account_group.create({"code_prefix_start": "4", "name": "Group 4"}) + cls.group42 = account_group.create( + {"code_prefix_start": "42", "name": "Group 4", "parent_id": cls.group4.id} + ) + + cls.expense_account = cls.env["account.account"].create( + { + "name": "Expenses Account", + "code": "5000", + "account_type": "expense", + "company_id": cls.env.user.company_id.id, + "group_id": cls.group5.id, + } + ) + cls.income_account = cls.env["account.account"].create( + { + "name": "Income Account", + "code": "4000", + "account_type": "income", + "company_id": cls.env.user.company_id.id, + "group_id": cls.group4.id, + } + ) + cls.income_account_2 = cls.env["account.account"].create( + { + "name": "Income Account 2", + "code": "4200", + "account_type": "income", + "company_id": cls.env.user.company_id.id, + "group_id": cls.group42.id, + } + ) + + cls.aaa_1 = cls.env["account.analytic.account"].create( + {"name": "Account 1", "plan_id": cls.analytic_plan_1.id} + ) + + cls.aaa_2 = cls.env["account.analytic.account"].create( + {"name": "Account 2", "plan_id": cls.analytic_plan_1.id} + ) + cls.account_field = cls.analytic_plan_1._column_name() + cls.aal_1 = cls.env["account.analytic.line"].create( + { + "name": "aal 1", + cls.account_field: cls.aaa_1.id, + "general_account_id": cls.expense_account.id, + "amount": -150.0, + "date": "2024-09-30", + } + ) + cls.aal_2 = cls.env["account.analytic.line"].create( + { + "name": "aal 1", + cls.account_field: cls.aaa_2.id, + "general_account_id": cls.expense_account.id, + "amount": -50, + "date": "2024-11-30", + } + ) + cls.aal_3 = cls.env["account.analytic.line"].create( + { + "name": "aal 1", + cls.account_field: cls.aaa_2.id, + "general_account_id": cls.income_account.id, + "amount": 250, + "date": "2024-12-31", + } + ) + + cls.date_from = "2024-10-01" + cls.date_to = "2024-12-31" + cls.fy_start_date = "2024-01-01" + + def _get_report_lines( + self, account_ids=False, show_hierarchy=False, group_by_analytic_account=False + ): + company = self.env.user.company_id + trial_analytic_balance = self.env["ac.trial.balance.report.wizard"].create( + { + "date_from": self.date_from, + "date_to": self.date_to, + "show_hierarchy": show_hierarchy, + "company_id": company.id, + "account_ids": account_ids, + "fy_start_date": self.fy_start_date, + "plan_id": self.analytic_plan_1.id, + "group_by_analytic_account": group_by_analytic_account, + } + ) + data = trial_analytic_balance._prepare_report_trial_balance_analytic() + res_data = self.env[ + "report.account_analytic_report.trial_balance_analytic" + ]._get_report_values(trial_analytic_balance, data) + return res_data + + def _accounts_in_report(self, trial_balance): + accounts_in_report = [] + for account in trial_balance: + accounts_in_report.append(account["id"]) + + return accounts_in_report + + def _check_total_amounts_by_acc_type( + self, totals_by_acc_type, include_both_accounts=False + ): + for type_name, total_by_acc_type in totals_by_acc_type.items(): + if type_name == self.account_type_map["expense"]: + self.assertTrue(total_by_acc_type["total_initial_balance"] == -150) + self.assertTrue(total_by_acc_type["total_ending_balance"] == -200) + + if include_both_accounts: + for aaa_id, amount in total_by_acc_type[ + "total_period_balance" + ].items(): + if aaa_id == self.aaa_2.id: + self.assertEqual(amount, -50) + else: + self.assertEqual(amount, 0) + else: + self.assertTrue(total_by_acc_type["total_period_balance"] == -50) + elif type_name == self.account_type_map["income"]: + self.assertTrue(total_by_acc_type["total_initial_balance"] == 0) + self.assertTrue(total_by_acc_type["total_ending_balance"] == 250) + if include_both_accounts: + for aaa_id, amount in total_by_acc_type[ + "total_period_balance" + ].items(): + if aaa_id == self.aaa_2.id: + self.assertEqual(amount, 250) + else: + self.assertEqual(amount, 0) + else: + self.assertTrue(total_by_acc_type["total_period_balance"] == 250) + + def test01_trial_analytic_balance(self): + res_data = self._get_report_lines() + trial_analytic_balance = res_data["trial_balance"] + accounts_in_report = self._accounts_in_report(trial_analytic_balance) + totals = res_data["total_amounts"] + + self.assertTrue(len(accounts_in_report) == 2) + self.assertTrue(self.expense_account.id in accounts_in_report) + self.assertTrue(self.income_account.id in accounts_in_report) + self.assertFalse(self.income_account_2.id in accounts_in_report) + + # Checks total amounts by account type + self._check_total_amounts_by_acc_type(res_data["totals_by_acc_type"]) + + # Checks total amounts + self.assertEqual(totals["total_initial_balance"], -150) + self.assertEqual(totals["total_period_balance"], -50 + 250) + self.assertEqual( + totals["total_ending_balance"], + -150 + -50 + 250, + ) + + # Check balances for every account + for account in trial_analytic_balance: + if account["id"] == self.income_account.id: + self.assertEqual(account["initial_balance"], 0) + self.assertEqual(account["balance"], 250) + self.assertEqual(account["ending_balance"], 250) + else: + self.assertEqual(account["initial_balance"], -150) + self.assertEqual(account["balance"], -50) + self.assertEqual(account["ending_balance"], -150 + -50) + + def test02_trial_analytic_balance_with_splited_accounts(self): + res_data = self._get_report_lines(account_ids=[self.aaa_1.id, self.aaa_2.id]) + trial_analytic_balance = res_data["trial_balance"] + totals = res_data["total_amounts"] + + accounts_in_report = self._accounts_in_report(trial_analytic_balance) + self.assertTrue(len(accounts_in_report) == 2) + self.assertTrue(self.expense_account.id in accounts_in_report) + self.assertTrue(self.income_account.id in accounts_in_report) + self.assertFalse(self.income_account_2.id in accounts_in_report) + + self.assertTrue(self.aaa_1.name in res_data["account_code_list"]) + self.assertTrue(self.aaa_2.name in res_data["account_code_list"]) + + # Checks total amounts by account type + self._check_total_amounts_by_acc_type( + res_data["totals_by_acc_type"], include_both_accounts=True + ) + + # Checks total amounts + self.assertEqual(totals["total_initial_balance"], -150) + self.assertEqual( + totals["total_ending_balance"], + -150 + -50 + 250, + ) + + for aaa_id, amount in totals["total_period_balance"].items(): + if aaa_id == self.aaa_2.id: + self.assertEqual(amount, -50 + 250) + else: + self.assertEqual(amount, 0) + + # Check balances for every account + for account in trial_analytic_balance: + if account["id"] == self.income_account.id: + self.assertEqual(account["initial_balance"], 0) + self.assertEqual(account["ending_balance"], 250) + for aaa_id, amount in account["accounts"].items(): + if aaa_id == self.aaa_2.id: + self.assertEqual(amount, 250) + else: + self.assertEqual(amount, 0) + else: + self.assertEqual(account["initial_balance"], -150) + self.assertEqual(account["ending_balance"], -150 + -50) + for aaa_id, amount in account["accounts"].items(): + if aaa_id == self.aaa_2.id: + self.assertEqual(amount, -50) + else: + self.assertEqual(amount, 0) + + def test03_trial_analytic_balance_gruped_by_analytic_account(self): + res_data = self._get_report_lines(group_by_analytic_account=True) + trial_analytic_balance = res_data["trial_balance"] + totals = res_data["total_amounts"] + accounts_in_report = self._accounts_in_report(trial_analytic_balance) + + self.assertTrue(len(accounts_in_report) == 2) + self.assertTrue(self.aaa_1.id in accounts_in_report) + self.assertTrue(self.aaa_2.id in accounts_in_report) + + self._check_total_amounts_by_acc_type(res_data["totals_by_acc_type"]) + self.assertEqual(totals["total_initial_balance"], -150) + self.assertEqual(totals["total_period_balance"], -50 + 250) + self.assertEqual( + totals["total_ending_balance"], + -150 + -50 + 250, + ) + + for account in trial_analytic_balance: + if account["id"] == self.aaa_1.id: + self.assertEqual(account["initial_balance"], -150) + self.assertEqual(account["balance"], 0) + self.assertEqual(account["ending_balance"], -150) + else: + self.assertEqual(account["initial_balance"], 0) + self.assertEqual(account["balance"], -50 + 250) + self.assertEqual(account["ending_balance"], -50 + 250) + + def test04_trial_analytic_balance_gruped_by_analytic_account_filtered(self): + res_data = self._get_report_lines( + group_by_analytic_account=True, account_ids=[self.aaa_1.id] + ) + trial_analytic_balance = res_data["trial_balance"] + totals = res_data["total_amounts"] + accounts_in_report = self._accounts_in_report(trial_analytic_balance) + + self.assertTrue(len(accounts_in_report) == 1) + self.assertTrue(self.aaa_1.id in accounts_in_report) + self.assertFalse(self.aaa_2.id in accounts_in_report) + + for type_name, total_by_acc_type in res_data["totals_by_acc_type"].items(): + if type_name == self.account_type_map["expense"]: + self.assertEqual(total_by_acc_type["total_initial_balance"], -150) + self.assertEqual(total_by_acc_type["total_period_balance"], 0) + self.assertEqual(total_by_acc_type["total_ending_balance"], -150) + self.assertEqual(totals["total_initial_balance"], -150) + self.assertEqual(totals["total_period_balance"], 0) + self.assertEqual(totals["total_ending_balance"], -150) + + for account in trial_analytic_balance: + if account["id"] == self.aaa_1.id: + self.assertEqual(account["initial_balance"], -150) + self.assertEqual(account["balance"], 0) + self.assertEqual(account["ending_balance"], -150) + + def test05_trial_analytic_balance_show_hirarchy(self): + self.env["account.analytic.line"].create( + { + "name": "aal 1", + self.account_field: self.aaa_2.id, + "general_account_id": self.income_account_2.id, + "amount": 300, + "date": "2024-12-31", + } + ) + res_data = self._get_report_lines(show_hierarchy=True) + trial_analytic_balance = res_data["trial_balance"] + accounts_in_report = self._accounts_in_report(trial_analytic_balance) + totals = res_data["total_amounts"] + + self.assertTrue(len(accounts_in_report) == 6) + self.assertTrue(self.expense_account.id in accounts_in_report) + self.assertTrue(self.income_account.id in accounts_in_report) + self.assertTrue(self.income_account_2.id in accounts_in_report) + + # Checks total amounts by account type + for type_name, total_by_acc_type in res_data["totals_by_acc_type"].items(): + if type_name == self.account_type_map["expense"]: + self.assertTrue(total_by_acc_type["total_initial_balance"] == -150) + self.assertTrue(total_by_acc_type["total_period_balance"] == -50) + self.assertTrue(total_by_acc_type["total_ending_balance"] == -200) + elif type_name == self.account_type_map["income"]: + self.assertTrue(total_by_acc_type["total_initial_balance"] == 0) + self.assertTrue(total_by_acc_type["total_period_balance"] == 250 + 300) + self.assertTrue(total_by_acc_type["total_ending_balance"] == 250 + 300) + + # Checks total amounts + self.assertEqual(totals["total_initial_balance"], -150) + self.assertEqual(totals["total_period_balance"], -50 + 250 + 300) + self.assertEqual(totals["total_ending_balance"], -150 + -50 + 250 + 300) + + # Check balances for every account + for account in trial_analytic_balance: + if account["type"] == "group_type": + if account["code"] == "4": + self.assertEqual(account["name"], "Group 4") + self.assertEqual(account["complete_code"], "4") + self.assertEqual(account["level"], 0) + self.assertTrue(self.income_account.id in account["account_ids"]) + self.assertEqual(account["initial_balance"], 0) + self.assertEqual(account["balance"], 250 + 300) + self.assertEqual(account["ending_balance"], 250 + 300) + if account["code"] == "42": + self.assertEqual(account["name"], "Group 4") + self.assertEqual(account["complete_code"], "4/42") + self.assertEqual(account["level"], 1) + self.assertTrue(self.income_account_2.id in account["account_ids"]) + self.assertEqual(account["initial_balance"], 0) + self.assertEqual(account["balance"], 300) + self.assertEqual(account["ending_balance"], 300) + if account["code"] == "5": + self.assertEqual(account["name"], "Group 5") + self.assertEqual(account["complete_code"], "5") + self.assertEqual(account["level"], 0) + self.assertTrue(self.expense_account.id in account["account_ids"]) + self.assertEqual(account["initial_balance"], -150) + self.assertEqual(account["balance"], -50) + self.assertEqual(account["ending_balance"], -150 + -50) + else: + if account["id"] == self.income_account.id: + self.assertEqual(account["initial_balance"], 0) + self.assertEqual(account["balance"], 250) + self.assertEqual(account["ending_balance"], 250) + elif account["id"] == self.income_account_2.id: + self.assertEqual(account["initial_balance"], 0) + self.assertEqual(account["balance"], 300) + self.assertEqual(account["ending_balance"], 300) + else: + self.assertEqual(account["initial_balance"], -150) + self.assertEqual(account["balance"], -50) + self.assertEqual(account["ending_balance"], -150 + -50) diff --git a/account_analytic_report/views/account_analytic_line.xml b/account_analytic_report/views/account_analytic_line.xml new file mode 100644 index 000000000000..08ae84584501 --- /dev/null +++ b/account_analytic_report/views/account_analytic_line.xml @@ -0,0 +1,14 @@ + + + + + + 1 + + diff --git a/account_analytic_report/views/report_trial_balance_analytic.xml b/account_analytic_report/views/report_trial_balance_analytic.xml new file mode 100644 index 000000000000..3322d05a8fb9 --- /dev/null +++ b/account_analytic_report/views/report_trial_balance_analytic.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/account_analytic_report/wizard/__init__.py b/account_analytic_report/wizard/__init__.py new file mode 100644 index 000000000000..801628b39d43 --- /dev/null +++ b/account_analytic_report/wizard/__init__.py @@ -0,0 +1 @@ +from . import trial_balance_analytic_wizard_view diff --git a/account_analytic_report/wizard/trial_balance_analytic_wizard_view.py b/account_analytic_report/wizard/trial_balance_analytic_wizard_view.py new file mode 100644 index 000000000000..2ac782405e78 --- /dev/null +++ b/account_analytic_report/wizard/trial_balance_analytic_wizard_view.py @@ -0,0 +1,170 @@ +# Copyright 2024 (APSL - Nagarro) Bernat Obrador +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError +from odoo.tools import date_utils + + +class AnalyticTrialBalanceReportWizard(models.TransientModel): + """Trial balance report wizard.""" + + _name = "ac.trial.balance.report.wizard" + _description = "Analytic Trial Balance Report Wizard" + _inherit = "account_financial_report_abstract_wizard" + + date_range_id = fields.Many2one(comodel_name="date.range", string="Date range") + date_from = fields.Date() + date_to = fields.Date() + fy_start_date = fields.Date(compute="_compute_fy_start_date") + account_ids = fields.Many2many( + comodel_name="account.analytic.account", string="Filter accounts" + ) + plan_id = fields.Many2one( + "account.analytic.plan", domain="[('parent_id', '=', False)]" + ) + + group_by_analytic_account = fields.Boolean(string="Group by Analytic Account") + show_hierarchy = fields.Boolean(help="Shows hierarchy of the financial accounts") + limit_hierarchy_level = fields.Boolean(help="Limits hierarchy level") + hierarchy_level = fields.Integer(help="Hierarchy levels to show", default=1) + + show_months = fields.Boolean( + help=""" + This option works only when exporting to Excel. It will create a separate sheet + for each selected analytic account, displaying all financial accounts with a + balance. + For each account, it shows the monthly balance within the selected date range. + """ + ) + + @api.depends("date_from") + def _compute_fy_start_date(self): + for wiz in self: + if wiz.date_from: + date_from, date_to = date_utils.get_fiscal_year( + wiz.date_from, + day=self.company_id.fiscalyear_last_day, + month=int(self.company_id.fiscalyear_last_month), + ) + wiz.fy_start_date = date_from + else: + wiz.fy_start_date = False + + @api.onchange("company_id") + def onchange_company_id(self): + """Handle company change.""" + if ( + self.company_id + and self.date_range_id.company_id + and self.date_range_id.company_id != self.company_id + ): + self.date_range_id = False + + res = { + "domain": { + "date_range_id": [], + } + } + if not self.company_id: + return res + else: + # res["domain"]["account_ids"] += [("company_id", "=", self.company_id.id)] + res["domain"]["date_range_id"] += [ + "|", + ("company_id", "=", self.company_id.id), + ("company_id", "=", False), + ] + return res + + @api.onchange("date_range_id") + def onchange_date_range_id(self): + """Handle date range change.""" + self.date_from = self.date_range_id.date_start + self.date_to = self.date_range_id.date_end + + @api.onchange("group_by_analytic_account") + def onchange_group_by_analytic_account(self): + if self.group_by_analytic_account: + self._not_show_hierarchy() + + @api.onchange("plan_id") + def _onchange_plan_id(self): + if self.account_ids: + self.account_ids = False + self.show_months = False + + @api.constrains("company_id", "date_range_id") + def _check_company_id_date_range_id(self): + for rec in self.sudo(): + if ( + rec.company_id + and rec.date_range_id.company_id + and rec.company_id != rec.date_range_id.company_id + ): + raise ValidationError( + _( + "The Company in the Trial Balance Report Wizard and in " + "Date Range must be the same." + ) + ) + + @api.constrains("show_hierarchy", "hierarchy_level") + def _check_show_hierarchy_level(self): + for rec in self: + if rec.show_hierarchy and rec.hierarchy_level <= 0: + raise UserError( + _("The hierarchy level to filter on must be greater than 0.") + ) + + @api.onchange("account_ids") + def _onchange_account_ids(self): + if self.account_ids: + self._not_show_hierarchy() + + def _print_report(self, report_type): + self.ensure_one() + data = self._prepare_report_trial_balance_analytic() + if report_type == "xlsx": + report_name = "a_f_r.report_trial_balance_analytic_xlsx" + else: + report_name = "account_analytic_report.trial_balance_analytic" + + return ( + self.env["ir.actions.report"] + .search( + [("report_name", "=", report_name), ("report_type", "=", report_type)], + limit=1, + ) + .report_action(self, data=data) + ) + + def _not_show_hierarchy(self): + self.show_hierarchy = False + self.limit_hierarchy_level = False + self.hierarchy_level = 1 + + def _prepare_report_trial_balance_analytic(self): + self.ensure_one() + sorted_accounts_ids = sorted([account.id for account in self.account_ids]) + return { + "wizard_id": self.id, + "date_from": self.date_from, + "date_to": self.date_to, + "company_id": self.company_id.id, + "account_ids": sorted_accounts_ids or [], + "fy_start_date": self.fy_start_date, + "account_financial_report_lang": self.env.lang, + "plan_field": self.plan_id._column_name(), + "plan_name": self.plan_id.name, + "plan_id": self.plan_id.id, + "group_by_analytic_account": self.group_by_analytic_account, + "show_hierarchy": self.show_hierarchy, + "limit_hierarchy_level": self.limit_hierarchy_level, + "hierarchy_level": self.hierarchy_level, + "show_months": self.show_months, + } + + def _export(self, report_type): + """Default export is PDF.""" + return self._print_report(report_type) diff --git a/account_analytic_report/wizard/trial_balance_analytic_wizard_view.xml b/account_analytic_report/wizard/trial_balance_analytic_wizard_view.xml new file mode 100644 index 000000000000..ada2096b95fc --- /dev/null +++ b/account_analytic_report/wizard/trial_balance_analytic_wizard_view.xml @@ -0,0 +1,94 @@ + + + + + Analytic Trial Balance + ac.trial.balance.report.wizard + +
+ + + +
+ + + + + + + + + + + + + + + +
+ + +
+
+
+
+
+ + + + + Analytic Trial Balance + ac.trial.balance.report.wizard + form + + new + +