diff --git a/account_analytic_report/README.rst b/account_analytic_report/README.rst new file mode 100644 index 000000000000..ba9206b88f87 --- /dev/null +++ b/account_analytic_report/README.rst @@ -0,0 +1,105 @@ +======================== +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| + + + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + + + +Known issues / Roadmap +====================== + + + +Changelog +========= + + + +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..85244345e009 --- /dev/null +++ b/account_analytic_report/__manifest__.py @@ -0,0 +1,31 @@ +# 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", + "security/security.xml", + "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/CONFIGURE.md b/account_analytic_report/readme/CONFIGURE.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/account_analytic_report/readme/CONTRIBUTORS.md b/account_analytic_report/readme/CONTRIBUTORS.md new file mode 100644 index 000000000000..fd6acfe2c7c8 --- /dev/null +++ b/account_analytic_report/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- [APSL-Nagarro](https://apsl.tech): + - Bernat Obrador + - Miquel Alzanillas \ No newline at end of file diff --git a/account_analytic_report/readme/DESCRIPTION.md b/account_analytic_report/readme/DESCRIPTION.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/account_analytic_report/readme/HISTORY.md b/account_analytic_report/readme/HISTORY.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/account_analytic_report/readme/ROADMAP.md b/account_analytic_report/readme/ROADMAP.md new file mode 100644 index 000000000000..e69de29bb2d1 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/security/security.xml b/account_analytic_report/security/security.xml new file mode 100644 index 000000000000..56d2f04b3b9a --- /dev/null +++ b/account_analytic_report/security/security.xml @@ -0,0 +1,3 @@ + + + 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..fab19003b16b --- /dev/null +++ b/account_analytic_report/static/description/index.html @@ -0,0 +1,440 @@ + + + + + +Account Analytic Reports + + + +
+

Account Analytic Reports

+ + +

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

+

Table of contents

+ + + + +
+

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..dc0eb6938ba9 --- /dev/null +++ b/account_analytic_report/tests/test_trial_analytic_balance.py @@ -0,0 +1,287 @@ +# 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", + } + ) + + cls.expense_account = cls.env["account.account"].create( + { + "name": "Expenses Account", + "code": "5000", + "account_type": "expense", + "company_id": cls.env.user.company_id.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, + } + ) + 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, + } + ) + + 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} + ) + account_field = cls.analytic_plan_1._column_name() + cls.aal_1 = cls.env["account.analytic.line"].create( + { + "name": "aal 1", + 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", + 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", + 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 test03_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) 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..0aeca39833f5 --- /dev/null +++ b/account_analytic_report/views/account_analytic_line.xml @@ -0,0 +1,12 @@ + + + + + 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..e24ab35c2a94 --- /dev/null +++ b/account_analytic_report/views/report_trial_balance_analytic.xml @@ -0,0 +1,9 @@ + + + + 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..26a8785b832e --- /dev/null +++ b/account_analytic_report/wizard/trial_balance_analytic_wizard_view.xml @@ -0,0 +1,93 @@ + + + + + Analytic Trial Balance + ac.trial.balance.report.wizard + +
+ + + +
+ + + + + + + + + + + + + + + +
+ + +
+
+
+
+
+ + + + + Analytic Trial Balance + ac.trial.balance.report.wizard + form + + new + +