diff --git a/account_operating_unit/README.rst b/account_operating_unit/README.rst new file mode 100644 index 0000000000..f0bd5598b0 --- /dev/null +++ b/account_operating_unit/README.rst @@ -0,0 +1,163 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +=============================== +Accounting with Operating Units +=============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:0eb4a10dd13a9c7cd234be254acda3395c49bbf3b03de94f05155aa85ce1c1e6 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Foperating--unit-lightgray.png?logo=github + :target: https://github.com/OCA/operating-unit/tree/19.0/account_operating_unit + :alt: OCA/operating-unit +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/operating-unit-19-0/operating-unit-19-0-account_operating_unit + :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/operating-unit&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows a company to manage the accounting based on Operating +Units (OU's). + +- The financial reports (Trial Balance, P&L, Balance Sheet), allow to + report the balances of one or more OU's. +- If a company wishes to report Balance Sheet and P&L accounts based on + OU's, they should indicate at company level that the OU's are + self-balanced, and the corresponding Inter-Operating Unit clearing + account. The Chart of Accounts will always be balanced, for each + Operating Unit. +- A company considering Operating Unit as applicable to report only + profits and losses will not need to set the OU's as self-balanced. +- The self-balancing of Operating Unit is ensured at the time of posting + a journal entry. In case that the journal involves posting of items in + separate Operating Units, new journal items will be created, using the + Inter-Operating Unit clearing account, to ensure that each OU is going + to be self-balanced for that journal entry. +- Adds the Operating Unit to the invoice. A user can choose what OU to + create the invoice for. +- Adds the Operating Unit to payments and payment methods. The operating + unit of a payment will be that of the payment method chosen. +- Implements security rules at OU level to invoices, payments and + journal items. +- Adds the Operating Unit to the cash basis journal entries. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +If your company is required to generate a balanced balance sheet by +Operating Unit you can specify at company level that Operating Units +should be self-balanced, and then indicate a self-balancing clearing +account. + +1. Create an account "Inter-OU Clearing". It is a balance sheet account. +2. Go to *Settings / Companies / Configuration* and Set the "Operating + Units are self-balanced" checkbox. Then set the "Inter-OU Clearing" + account in "Inter-Operating Unit clearing account" field. +3. Go to *Accounting / Configuration / Accounting / Journals* and + define, for each Payment Method, the Operating Unit that will be used + in payments. + +Usage +===== + +- Add the Operating Unit to invoices. +- Report invoices by Operating Unit in *Accounting / Reporting* + *Business Intelligence / Invoices* +- Add the Default Operating Unit to account move. Then all move lines + will by default adopt this Operating Unit. +- Add Operating Units to the move lines. If they differ across lines of + the same move, and the OU's are self-balanced, then additional move + lines will be created so as to make the move self-balanced from OU + perspective. +- In the menu *Accounting / Reporting / PDF Reports*, you can indicate + the Operating Units to report on, for the *Trial Balance*, *Balance + Sheet*, *Profit and Loss*, and *Financial Reports*. + +Known issues / Roadmap +====================== + +- The *General Ledger*, *Aged Partner Balance* reports do not support + the filter by Operating Unit. Basically due to lack of proper hooks in + the standard methods used by these reports, to introduce the ability + to filter by Operating Unit. +- Trial Balance, P&L and Balance Sheet were removed from Odoo Community. + Once OCA Financial Reports are migrated to 13 we can add the Operating + Unit to those reports. + +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 +------- + +* ForgeFlow +* Serpent Consulting Services Pvt. Ltd. +* WilldooIT Pty Ltd + +Contributors +------------ + +- ForgeFlow +- Jordi Ballester Alomar +- Aarón Henríquez +- Serpent Consulting Services Pvt. Ltd. +- WilldooIT Pty Ltd +- Michael Villamar +- Jarsa Sistemas +- Alan Ramos +- Saran Lim. +- Pimolnat Suntian +- Hieu, Vo Minh Bao +- `Heliconia Solutions Pvt. Ltd. `__ + + - Bhavesh Heliconia + +- Julien Coux + +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. + +This module is part of the `OCA/operating-unit `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_operating_unit/__init__.py b/account_operating_unit/__init__.py new file mode 100644 index 0000000000..1fc45468f5 --- /dev/null +++ b/account_operating_unit/__init__.py @@ -0,0 +1,4 @@ +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from . import models +from . import report diff --git a/account_operating_unit/__manifest__.py b/account_operating_unit/__manifest__.py new file mode 100644 index 0000000000..e53f3a9ab9 --- /dev/null +++ b/account_operating_unit/__manifest__.py @@ -0,0 +1,29 @@ +# © 2019 ForgeFlow S.L. +# © 2019 Serpent Consulting Services Pvt. Ltd. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). +{ + "name": "Accounting with Operating Units", + "summary": "Introduces Operating Unit (OU) in invoices and " + "Accounting Entries with clearing account", + "version": "19.0.1.0.0", + "author": "ForgeFlow, " + "Serpent Consulting Services Pvt. Ltd.," + "WilldooIT Pty Ltd," + "Odoo Community Association (OCA)", + "website": "https://github.com/OCA/operating-unit", + "category": "Accounting & Finance", + "depends": [ + "account", + "analytic_operating_unit", + "base_view_inheritance_extension", + ], + "license": "LGPL-3", + "data": [ + "security/account_security.xml", + "views/account_move_view.xml", + "views/account_journal_view.xml", + "views/company_view.xml", + "views/account_payment_view.xml", + "views/account_invoice_report_view.xml", + ], +} diff --git a/account_operating_unit/i18n/account_operating_unit.pot b/account_operating_unit/i18n/account_operating_unit.pot new file mode 100644 index 0000000000..cdf8312092 --- /dev/null +++ b/account_operating_unit/i18n/account_operating_unit.pot @@ -0,0 +1,154 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_operating_unit +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: account_operating_unit +#: model:ir.model.fields,help:account_operating_unit.field_res_company__ou_is_self_balanced +msgid "" +"Activate if your company is required to generate a balanced balance sheet " +"for each operating unit." +msgstr "" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_account_bank_statement_line +msgid "Bank Statement Line" +msgstr "" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_res_company +msgid "Companies" +msgstr "" + +#. module: account_operating_unit +#. odoo-python +#: code:addons/account_operating_unit/models/account_journal.py:0 +msgid "" +"Configuration error. If defined as self-balanced at company level, the " +"operating unit is mandatory in bank journal." +msgstr "" + +#. module: account_operating_unit +#. odoo-python +#: code:addons/account_operating_unit/models/res_company.py:0 +msgid "" +"Configuration error. Please provide an Inter-operating unit clearing " +"account." +msgstr "" + +#. module: account_operating_unit +#. odoo-python +#: code:addons/account_operating_unit/models/account_move.py:0 +msgid "" +"Configuration error. The Operating Unit in the Move Line and in the Move " +"must be the same." +msgstr "" + +#. module: account_operating_unit +#. odoo-python +#: code:addons/account_operating_unit/models/account_move.py:0 +msgid "" +"Configuration error. The operating unit is mandatory for each line as the " +"operating unit has been defined as self-balanced at company level." +msgstr "" + +#. module: account_operating_unit +#. odoo-python +#: code:addons/account_operating_unit/models/account_move.py:0 +msgid "" +"Configuration error. You need to define aninter-operating unit clearing " +"account in the company settings" +msgstr "" + +#. module: account_operating_unit +#: model:ir.model.fields,field_description:account_operating_unit.field_res_company__inter_ou_clearing_account_id +msgid "Inter-operating unit clearing account" +msgstr "" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_account_invoice_report +msgid "Invoices Statistics" +msgstr "" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_account_journal +msgid "Journal" +msgstr "" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_account_move +msgid "Journal Entry" +msgstr "" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_account_move_line +msgid "Journal Item" +msgstr "" + +#. module: account_operating_unit +#. odoo-python +#: code:addons/account_operating_unit/models/account_move.py:0 +msgid "OU-Balancing" +msgstr "" + +#. module: account_operating_unit +#: model:ir.model.fields,field_description:account_operating_unit.field_account_bank_statement_line__operating_unit_id +#: model:ir.model.fields,field_description:account_operating_unit.field_account_invoice_report__operating_unit_id +#: model:ir.model.fields,field_description:account_operating_unit.field_account_journal__operating_unit_id +#: model:ir.model.fields,field_description:account_operating_unit.field_account_move__operating_unit_id +#: model:ir.model.fields,field_description:account_operating_unit.field_account_move_line__operating_unit_id +#: model:ir.model.fields,field_description:account_operating_unit.field_account_payment__operating_unit_id +#: model_terms:ir.ui.view,arch_db:account_operating_unit.view_account_invoice_filter +#: model_terms:ir.ui.view,arch_db:account_operating_unit.view_account_invoice_report_search +#: model_terms:ir.ui.view,arch_db:account_operating_unit.view_account_move_line_filter +#: model_terms:ir.ui.view,arch_db:account_operating_unit.view_account_payment_search +msgid "Operating Unit" +msgstr "" + +#. module: account_operating_unit +#: model:ir.model.fields,help:account_operating_unit.field_account_journal__operating_unit_id +msgid "" +"Operating Unit that will be used in payments, when this journal is used." +msgstr "" + +#. module: account_operating_unit +#: model_terms:ir.ui.view,arch_db:account_operating_unit.view_company_form +msgid "Operating Units" +msgstr "" + +#. module: account_operating_unit +#: model:ir.model.fields,field_description:account_operating_unit.field_res_company__ou_is_self_balanced +msgid "Operating Units are self-balanced" +msgstr "" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_account_partial_reconcile +msgid "Partial Reconcile" +msgstr "" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_account_payment +msgid "Payments" +msgstr "" + +#. module: account_operating_unit +#. odoo-python +#: code:addons/account_operating_unit/models/account_move.py:0 +msgid "The OU in the Move and in Journal must be the same." +msgstr "" + +#. module: account_operating_unit +#: model:ir.model.fields,help:account_operating_unit.field_account_bank_statement_line__operating_unit_id +#: model:ir.model.fields,help:account_operating_unit.field_account_move__operating_unit_id +msgid "This operating unit will be defaulted in the move lines." +msgstr "" diff --git a/account_operating_unit/i18n/es_MX.po b/account_operating_unit/i18n/es_MX.po new file mode 100644 index 0000000000..22e8b8f48e --- /dev/null +++ b/account_operating_unit/i18n/es_MX.po @@ -0,0 +1,198 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_operating_unit +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-03-21 13:02+0000\n" +"Last-Translator: Jesús Alan Ramos Rodríguez \n" +"Language-Team: none\n" +"Language: es_MX\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.14.1\n" + +#. module: account_operating_unit +#: model:ir.model.fields,help:account_operating_unit.field_res_company__ou_is_self_balanced +msgid "" +"Activate if your company is required to generate a balanced balance sheet " +"for each operating unit." +msgstr "" +"Actívelo si su empresa está obligada a generar un balance general por cada " +"unidad operativa." + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_account_bank_statement_line +msgid "Bank Statement Line" +msgstr "Línea extracto bancario" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_res_company +msgid "Companies" +msgstr "Empresas" + +#. module: account_operating_unit +#. odoo-python +#: code:addons/account_operating_unit/models/account_journal.py:0 +#, python-format +msgid "" +"Configuration error. If defined as self-balanced at company level, the " +"operating unit is mandatory in bank journal." +msgstr "" +"Error de configuración. Si se define como auto-balanceado a nivel de " +"empresa, la unidad operativa es obligatoria en el diario bancario." + +#. module: account_operating_unit +#. odoo-python +#: code:addons/account_operating_unit/models/res_company.py:0 +#, python-format +msgid "" +"Configuration error. Please provide an Inter-operating unit clearing account." +msgstr "" +"Error de configuración. Proporcione una cuenta de compensación de unidades " +"interoperativas." + +#. module: account_operating_unit +#. odoo-python +#: code:addons/account_operating_unit/models/account_move.py:0 +#, python-format +msgid "" +"Configuration error. The Operating Unit in the Move Line and in the Move " +"must be the same." +msgstr "" +"Error de configuración. La Unidad Operativa en la Línea de Movimiento y en " +"el Movimiento debe ser la misma." + +#. module: account_operating_unit +#. odoo-python +#: code:addons/account_operating_unit/models/account_move.py:0 +#, python-format +msgid "" +"Configuration error. The operating unit is mandatory for each line as the " +"operating unit has been defined as self-balanced at company level." +msgstr "" +"Error de configuración. La unidad operativa es obligatoria para cada línea " +"ya que la unidad operativa se ha definido como autobalanceada a nivel de " +"empresa." + +#. module: account_operating_unit +#. odoo-python +#: code:addons/account_operating_unit/models/account_move.py:0 +#, python-format +msgid "" +"Configuration error. You need to define aninter-operating unit clearing " +"account in the company settings" +msgstr "" +"Error de configuración. Debe definir una cuenta de compensación entre " +"unidades operativas en la configuración de la empresa." + +#. module: account_operating_unit +#: model:ir.model.fields,field_description:account_operating_unit.field_res_company__inter_ou_clearing_account_id +msgid "Inter-operating unit clearing account" +msgstr "Cuenta de compensación de unidades interoperativas" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_account_invoice_report +msgid "Invoices Statistics" +msgstr "Estadísticas de facturas" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_account_journal +msgid "Journal" +msgstr "Diario" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_account_move +msgid "Journal Entry" +msgstr "Asiento de diario" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_account_move_line +msgid "Journal Item" +msgstr "Apunte de diario" + +#. module: account_operating_unit +#. odoo-python +#: code:addons/account_operating_unit/models/account_move.py:0 +#, python-format +msgid "OU-Balancing" +msgstr "Equilibrio de OU" + +#. module: account_operating_unit +#: model:ir.model.fields,field_description:account_operating_unit.field_account_bank_statement_line__operating_unit_id +#: model:ir.model.fields,field_description:account_operating_unit.field_account_invoice_report__operating_unit_id +#: model:ir.model.fields,field_description:account_operating_unit.field_account_journal__operating_unit_id +#: model:ir.model.fields,field_description:account_operating_unit.field_account_move__operating_unit_id +#: model:ir.model.fields,field_description:account_operating_unit.field_account_move_line__operating_unit_id +#: model:ir.model.fields,field_description:account_operating_unit.field_account_payment__operating_unit_id +#: model_terms:ir.ui.view,arch_db:account_operating_unit.view_account_invoice_filter +#: model_terms:ir.ui.view,arch_db:account_operating_unit.view_account_invoice_report_search +#: model_terms:ir.ui.view,arch_db:account_operating_unit.view_account_move_line_filter +#: model_terms:ir.ui.view,arch_db:account_operating_unit.view_account_payment_search +msgid "Operating Unit" +msgstr "Unidad Operativa" + +#. module: account_operating_unit +#: model:ir.model.fields,help:account_operating_unit.field_account_journal__operating_unit_id +msgid "" +"Operating Unit that will be used in payments, when this journal is used." +msgstr "" +"Unidad Operativa que se utilizará en los pagos, cuando se utilice este " +"diario." + +#. module: account_operating_unit +#: model_terms:ir.ui.view,arch_db:account_operating_unit.view_company_form +msgid "Operating Units" +msgstr "Unidades operativas" + +#. module: account_operating_unit +#: model:ir.model.fields,field_description:account_operating_unit.field_res_company__ou_is_self_balanced +msgid "Operating Units are self-balanced" +msgstr "Las unidades operativas son autobalanceadas" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_account_partial_reconcile +msgid "Partial Reconcile" +msgstr "Reconciliación parcial" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_account_payment +msgid "Payments" +msgstr "Pagos" + +#. module: account_operating_unit +#. odoo-python +#: code:addons/account_operating_unit/models/account_move.py:0 +#, python-format +msgid "The OU in the Move and in Journal must be the same." +msgstr "La OU en el movimiento y en diario debe ser la misma." + +#. module: account_operating_unit +#: model:ir.model.fields,help:account_operating_unit.field_account_bank_statement_line__operating_unit_id +#: model:ir.model.fields,help:account_operating_unit.field_account_move__operating_unit_id +msgid "This operating unit will be defaulted in the move lines." +msgstr "Esta unidad operativa estará por defecto en las líneas de movimiento." + +#, python-format +#~ msgid "" +#~ "Configuration error. The Company in the Move Line and in the Operating " +#~ "Unit must be the same." +#~ msgstr "" +#~ "Error de configuración. La Empresa en la Línea de Movimiento y en la " +#~ "Unidad Operativa debe ser la misma." + +#, python-format +#~ msgid "The Company in the Move and in Operating Unit must be the same." +#~ msgstr "" +#~ "La Empresa en el movimiento y en la Unidad Operativa debe ser la misma." + +#~ msgid "Register Payment" +#~ msgstr "Registrar pago" + +#, python-format +#~ msgid "The OU in the Bills/Invoices to register payment must be the same." +#~ msgstr "La OU en las Facturas para registrar el pago debe ser la misma." diff --git a/account_operating_unit/i18n/it.po b/account_operating_unit/i18n/it.po new file mode 100644 index 0000000000..6a82c6732c --- /dev/null +++ b/account_operating_unit/i18n/it.po @@ -0,0 +1,179 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_operating_unit +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-08-05 09:58+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.6.2\n" + +#. module: account_operating_unit +#: model:ir.model.fields,help:account_operating_unit.field_res_company__ou_is_self_balanced +msgid "" +"Activate if your company is required to generate a balanced balance sheet " +"for each operating unit." +msgstr "" +"Da attivare se la tua azienda è tenuta a generare un bilancio in pareggio " +"per ogni Unità Operativa." + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_account_bank_statement_line +msgid "Bank Statement Line" +msgstr "Riga estratto conto" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_res_company +msgid "Companies" +msgstr "Aziende" + +#. module: account_operating_unit +#. odoo-python +#: code:addons/account_operating_unit/models/account_journal.py:0 +#, python-format +msgid "" +"Configuration error. If defined as self-balanced at company level, the " +"operating unit is mandatory in bank journal." +msgstr "" +"Errore di configurazione. Se definita come autobilanciata a livello " +"aziendale, l'unità operativa è obbligatoria nel registro banca." + +#. module: account_operating_unit +#. odoo-python +#: code:addons/account_operating_unit/models/res_company.py:0 +#, python-format +msgid "" +"Configuration error. Please provide an Inter-operating unit clearing " +"account." +msgstr "" +"Errore di configurazione. Fornire un conto di compensazione tra unità " +"operative." + +#. module: account_operating_unit +#. odoo-python +#: code:addons/account_operating_unit/models/account_move.py:0 +#, python-format +msgid "" +"Configuration error. The Operating Unit in the Move Line and in the Move " +"must be the same." +msgstr "" +"Errore di configurazione. L'unità operativa nella riga di movimento e e nel " +"movimento deve essere la stessa." + +#. module: account_operating_unit +#. odoo-python +#: code:addons/account_operating_unit/models/account_move.py:0 +#, python-format +msgid "" +"Configuration error. The operating unit is mandatory for each line as the " +"operating unit has been defined as self-balanced at company level." +msgstr "" +"Errore di configurazione. L'unità operativa è obbligatoria per ciascuna " +"linea in quanto l'unità operativa è stata definita autobilanciata a livello " +"aziendale." + +#. module: account_operating_unit +#. odoo-python +#: code:addons/account_operating_unit/models/account_move.py:0 +#, python-format +msgid "" +"Configuration error. You need to define aninter-operating unit clearing " +"account in the company settings" +msgstr "" +"Errore di configurazione. È necessario definire un conto di compensazione " +"tra unità operative nelle impostazioni dell'azienda" + +#. module: account_operating_unit +#: model:ir.model.fields,field_description:account_operating_unit.field_res_company__inter_ou_clearing_account_id +msgid "Inter-operating unit clearing account" +msgstr "Conto di compensazione tra unità operative" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_account_invoice_report +msgid "Invoices Statistics" +msgstr "Statistiche fatture" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_account_journal +msgid "Journal" +msgstr "Registro" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_account_move +msgid "Journal Entry" +msgstr "Registrazione contabile" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_account_move_line +msgid "Journal Item" +msgstr "Movimento contabile" + +#. module: account_operating_unit +#. odoo-python +#: code:addons/account_operating_unit/models/account_move.py:0 +#, python-format +msgid "OU-Balancing" +msgstr "Bilanciamento unità operativa" + +#. module: account_operating_unit +#: model:ir.model.fields,field_description:account_operating_unit.field_account_bank_statement_line__operating_unit_id +#: model:ir.model.fields,field_description:account_operating_unit.field_account_invoice_report__operating_unit_id +#: model:ir.model.fields,field_description:account_operating_unit.field_account_journal__operating_unit_id +#: model:ir.model.fields,field_description:account_operating_unit.field_account_move__operating_unit_id +#: model:ir.model.fields,field_description:account_operating_unit.field_account_move_line__operating_unit_id +#: model:ir.model.fields,field_description:account_operating_unit.field_account_payment__operating_unit_id +#: model_terms:ir.ui.view,arch_db:account_operating_unit.view_account_invoice_filter +#: model_terms:ir.ui.view,arch_db:account_operating_unit.view_account_invoice_report_search +#: model_terms:ir.ui.view,arch_db:account_operating_unit.view_account_move_line_filter +#: model_terms:ir.ui.view,arch_db:account_operating_unit.view_account_payment_search +msgid "Operating Unit" +msgstr "Unità operativa" + +#. module: account_operating_unit +#: model:ir.model.fields,help:account_operating_unit.field_account_journal__operating_unit_id +msgid "" +"Operating Unit that will be used in payments, when this journal is used." +msgstr "" +"L'unità operativa che verrà utilizzata nei pagamenti, quando viene " +"utilizzato questo registro." + +#. module: account_operating_unit +#: model_terms:ir.ui.view,arch_db:account_operating_unit.view_company_form +msgid "Operating Units" +msgstr "Unità operative" + +#. module: account_operating_unit +#: model:ir.model.fields,field_description:account_operating_unit.field_res_company__ou_is_self_balanced +msgid "Operating Units are self-balanced" +msgstr "Le unità operative sono autobilanciate" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_account_partial_reconcile +msgid "Partial Reconcile" +msgstr "Riconciliazione parziale" + +#. module: account_operating_unit +#: model:ir.model,name:account_operating_unit.model_account_payment +msgid "Payments" +msgstr "Pagamenti" + +#. module: account_operating_unit +#. odoo-python +#: code:addons/account_operating_unit/models/account_move.py:0 +#, python-format +msgid "The OU in the Move and in Journal must be the same." +msgstr "L'unità operativa nel movimento e nel Registro deve essere la stessa." + +#. module: account_operating_unit +#: model:ir.model.fields,help:account_operating_unit.field_account_bank_statement_line__operating_unit_id +#: model:ir.model.fields,help:account_operating_unit.field_account_move__operating_unit_id +msgid "This operating unit will be defaulted in the move lines." +msgstr "Questa unità operativa sarà predefinita nelle righe di movimento." diff --git a/account_operating_unit/models/__init__.py b/account_operating_unit/models/__init__.py new file mode 100644 index 0000000000..51a95d2831 --- /dev/null +++ b/account_operating_unit/models/__init__.py @@ -0,0 +1,8 @@ +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from . import res_company +from . import account_bank_statement +from . import account_journal +from . import account_move +from . import account_partial_reconcile +from . import account_payment diff --git a/account_operating_unit/models/account_bank_statement.py b/account_operating_unit/models/account_bank_statement.py new file mode 100644 index 0000000000..3e6a40ad39 --- /dev/null +++ b/account_operating_unit/models/account_bank_statement.py @@ -0,0 +1,20 @@ +# Copyright 2022 Jarsa +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import models + + +class AccountBankStatementLine(models.Model): + _inherit = "account.bank.statement.line" + + def _prepare_move_line_default_vals(self, counterpart_account_id=None): + result = super()._prepare_move_line_default_vals( + counterpart_account_id=counterpart_account_id + ) + result[0]["operating_unit_id"] = ( + self.statement_id.journal_id.operating_unit_id.id + ) + result[1]["operating_unit_id"] = ( + self.statement_id.journal_id.operating_unit_id.id + ) + return result diff --git a/account_operating_unit/models/account_journal.py b/account_operating_unit/models/account_journal.py new file mode 100644 index 0000000000..2f122eecb8 --- /dev/null +++ b/account_operating_unit/models/account_journal.py @@ -0,0 +1,33 @@ +# © 2019 ForgeFlow S.L. +# © 2019 Serpent Consulting Services Pvt. Ltd. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class AccountJournal(models.Model): + _inherit = "account.journal" + + operating_unit_id = fields.Many2one( + check_company=True, + comodel_name="operating.unit", + help="Operating Unit that will be used in payments, when this journal is used.", + ) + + @api.constrains("type") + def _check_ou(self): + for journal in self: + if ( + journal.type in ("bank", "cash") + and journal.company_id.ou_is_self_balanced + and not journal.operating_unit_id + ): + raise UserError( + self.env._( + "Configuration error. If defined as " + "self-balanced at company level, the " + "operating unit is mandatory in bank " + "journal." + ) + ) diff --git a/account_operating_unit/models/account_move.py b/account_operating_unit/models/account_move.py new file mode 100644 index 0000000000..a6fdeb6e9b --- /dev/null +++ b/account_operating_unit/models/account_move.py @@ -0,0 +1,251 @@ +# © 2019 ForgeFlow S.L. +# © 2019 Serpent Consulting Services Pvt. Ltd. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models +from odoo.exceptions import UserError +from odoo.fields import Command, Domain + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + + operating_unit_id = fields.Many2one( + check_company=True, + comodel_name="operating.unit", + ) + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get("move_id", False): + move = self.env["account.move"].browse(vals["move_id"]) + if move.operating_unit_id: + vals["operating_unit_id"] = move.operating_unit_id.id + return super().create(vals_list) + + @api.constrains("operating_unit_id", "move_id") + def _check_move_operating_unit(self): + for rec in self: + if ( + rec.move_id + and rec.move_id.operating_unit_id + and rec.operating_unit_id + and rec.move_id.operating_unit_id != rec.operating_unit_id + ): + raise UserError( + self.env._( + "Configuration error. The Operating Unit in" + " the Move Line and in the Move must be the" + " same." + ) + ) + + def _check_ou_balance(self, lines): + # Look for the balance of each OU + ou_balance = {} + for line in lines: + if line.operating_unit_id.id not in ou_balance: + ou_balance[line.operating_unit_id.id] = 0.0 + ou_balance[line.operating_unit_id.id] += line.credit - line.debit + return ou_balance + + def reconcile(self): + # if one OU pays the invoices of different OU + # a regularization entry must be created (this + # was a feature in version <= 12) + if self and not self[0].company_id.ou_is_self_balanced: + return super().reconcile() + bank_journal = self.mapped("move_id.journal_id").filtered( + lambda jl: jl.type in ("bank", "cash") + ) + if not bank_journal: + return super().reconcile() + bank_journal = bank_journal[0] + # If all move lines point to the same operating unit, there's no + # need to create a balancing move line + if len(self.mapped("operating_unit_id")) <= 1: + return super().reconcile() + # Create balancing entries for un-balanced OU's. + move_vals = self._prepare_inter_ou_balancing_move(bank_journal) + move = self.env["account.move"].create(move_vals) + ou_balances = self._check_ou_balance(self) + amls = self.env["account.move.line"] + line_datas = [] + for ou_id in list(ou_balances.keys()): + # If the OU is already balanced, then do not continue + if move.company_id.currency_id.is_zero(ou_balances[ou_id]): + continue + # Create a balancing move line in the operating unit + # clearing account + line_data = move._prepare_inter_ou_balancing_move_line( + move, ou_id, ou_balances + ) + if line_data: + line_datas.append(line_data) + if line_datas: + amls = self.with_context(check_move_validity=False).create(line_datas) + if amls: + move.with_context(check_move_validity=True).write( + {"line_ids": [Command.link(aml.id) for aml in amls]} + ) + move.with_context(inter_ou_balance_entry=True).action_post() + return super().reconcile() + + def _prepare_inter_ou_balancing_move(self, journal): + move_vals = { + "journal_id": journal.id, + "date": max(self.mapped("date")), + "ref": "Inter OU Balancing", + "company_id": journal.company_id.id, + } + return move_vals + + +class AccountMove(models.Model): + _inherit = "account.move" + + operating_unit_id = fields.Many2one( + comodel_name="operating.unit", + default=lambda self: self._default_operating_unit_id(), + help="This operating unit will be defaulted in the move lines.", + readonly=False, + compute="_compute_operating_unit", + store=True, + check_company=True, + ) + + @api.model + def _default_operating_unit_id(self): + if ( + default_type := self.env.context.get("default_move_type") + ) and default_type != "entry": + return self.env["res.users"]._get_default_operating_unit() + return False + + @api.onchange("operating_unit_id") + def _onchange_operating_unit(self): + if self.operating_unit_id and ( + not self.journal_id + or self.journal_id.operating_unit_id != self.operating_unit_id + ): + journal = self.env["account.journal"].search( + Domain("type", "=", self.journal_id.type) + ) + jf = journal.filtered( + lambda aj: aj.operating_unit_id == self.operating_unit_id + ) + if not jf: + self.journal_id = journal[0] + else: + self.journal_id = jf[0] + for line in self.line_ids: + line.operating_unit_id = self.operating_unit_id + + @api.depends("journal_id") + def _compute_operating_unit(self): + for record in self: + if record.journal_id.operating_unit_id: + record.operating_unit_id = record.journal_id.operating_unit_id + for line in record.line_ids: + line.operating_unit_id = record.journal_id.operating_unit_id + + def _prepare_inter_ou_balancing_move_line(self, move, ou_id, ou_balances): + if not move.company_id.inter_ou_clearing_account_id: + raise UserError( + self.env._( + "Configuration error. You need to define an" + "inter-operating unit clearing account in the " + "company settings" + ) + ) + + res = { + "name": self.env._("OU-Balancing"), + "move_id": move.id, + "journal_id": move.journal_id.id, + "date": move.date, + "operating_unit_id": ou_id, + "partner_id": move.partner_id and move.partner_id.id or False, + "account_id": move.company_id.inter_ou_clearing_account_id.id, + } + + if ou_balances[ou_id] < 0.0: + res["debit"] = abs(ou_balances[ou_id]) + else: + res["credit"] = ou_balances[ou_id] + return res + + def _check_ou_balance(self, move): + # Look for the balance of each OU + ou_balance = {} + for line in move.line_ids: + if line.operating_unit_id.id not in ou_balance: + ou_balance[line.operating_unit_id.id] = 0.0 + ou_balance[line.operating_unit_id.id] += line.debit - line.credit + return ou_balance + + def _post(self, soft=True): + ml_obj = self.env["account.move.line"] + for move in self: + if not move.company_id.ou_is_self_balanced or self.env.context.get( + "inter_ou_balance_entry", False + ): + continue + + # If all move lines point to the same operating unit, there's no + # need to create a balancing move line + if len(move.line_ids.operating_unit_id) <= 1: + continue + # Create balancing entries for un-balanced OU's. + ou_balances = self._check_ou_balance(move) + amls = self.env["account.move.line"] + line_datas = [] + for ou_id in list(ou_balances.keys()): + # If the OU is already balanced, then do not continue + if move.company_id.currency_id.is_zero(ou_balances[ou_id]): + continue + # Create a balancing move line in the operating unit + # clearing account + line_data = self._prepare_inter_ou_balancing_move_line( + move, ou_id, ou_balances + ) + if line_data: + line_datas.append(line_data) + if line_datas: + amls = ml_obj.with_context(check_move_validity=False).create(line_datas) + if amls: + move.with_context(check_move_validity=True).write( + {"line_ids": [Command.link(aml.id) for aml in amls]} + ) + + return super()._post(soft) + + @api.constrains("line_ids") + def _check_ou(self): + for move in self: + if not move.company_id.ou_is_self_balanced: + continue + for line in move.line_ids: + if not line.operating_unit_id: + raise UserError( + self.env._( + "Configuration error. The operating unit is " + "mandatory for each line as the operating unit " + "has been defined as self-balanced at company " + "level." + ) + ) + + @api.constrains("operating_unit_id", "journal_id") + def _check_journal_operating_unit(self): + for move in self: + if ( + move.journal_id.operating_unit_id + and move.operating_unit_id + and move.operating_unit_id != move.journal_id.operating_unit_id + ): + raise UserError( + self.env._("The OU in the Move and in Journal must be the same.") + ) + return True diff --git a/account_operating_unit/models/account_partial_reconcile.py b/account_operating_unit/models/account_partial_reconcile.py new file mode 100644 index 0000000000..cf6a82b45b --- /dev/null +++ b/account_operating_unit/models/account_partial_reconcile.py @@ -0,0 +1,40 @@ +# Copyright 2022 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from odoo import api, models + + +class AccountPartialReconcile(models.Model): + _inherit = "account.partial.reconcile" + + @api.model + def _prepare_cash_basis_base_line_vals(self, base_line, balance, amount_currency): + res = super()._prepare_cash_basis_base_line_vals( + base_line, balance, amount_currency + ) + res.update({"operating_unit_id": base_line.operating_unit_id.id}) + return res + + @api.model + def _prepare_cash_basis_counterpart_base_line_vals(self, cb_base_line_vals): + res = super()._prepare_cash_basis_counterpart_base_line_vals(cb_base_line_vals) + res.update({"operating_unit_id": cb_base_line_vals.get("operating_unit_id")}) + return res + + @api.model + def _prepare_cash_basis_tax_line_vals(self, tax_line, balance, amount_currency): + res = super()._prepare_cash_basis_tax_line_vals( + tax_line, balance, amount_currency + ) + res.update({"operating_unit_id": tax_line.operating_unit_id.id}) + return res + + @api.model + def _prepare_cash_basis_counterpart_tax_line_vals(self, tax_line, cb_tax_line_vals): + res = super()._prepare_cash_basis_counterpart_tax_line_vals( + tax_line, cb_tax_line_vals + ) + res.update( + {"operating_unit_id": cb_tax_line_vals.get("operating_unit_id", False)} + ) + return res diff --git a/account_operating_unit/models/account_payment.py b/account_operating_unit/models/account_payment.py new file mode 100644 index 0000000000..86a8809fbe --- /dev/null +++ b/account_operating_unit/models/account_payment.py @@ -0,0 +1,45 @@ +# © 2019 ForgeFlow S.L. +# © 2019 Serpent Consulting Services Pvt. Ltd. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models + + +class AccountPayment(models.Model): + _inherit = "account.payment" + + operating_unit_id = fields.Many2one( + check_company=True, + comodel_name="operating.unit", + compute="_compute_operating_unit_id", + store=True, + ) + + @api.depends("journal_id") + def _compute_operating_unit_id(self): + for payment in self.filtered("journal_id"): + payment.operating_unit_id = payment.journal_id.operating_unit_id + + def _prepare_move_line_default_vals( + self, write_off_line_vals=None, force_balance=None + ): + lines = super()._prepare_move_line_default_vals( + write_off_line_vals, force_balance + ) + for line in lines: + line["operating_unit_id"] = self.operating_unit_id.id + active_model = self.env.context.get("active_model", False) + if not active_model or active_model != "account.move": + return lines + invoices = self.env[self.env.context.get("active_model")].browse( + self.env.context.get("active_ids") + ) + invoices_ou = invoices.operating_unit_id + if invoices and len(invoices_ou) == 1 and invoices_ou != self.operating_unit_id: + destination_account_id = self.destination_account_id.id + for line in lines: + if not line.get("operating_unit_id", False) or ( + line["account_id"] == destination_account_id + ): + line["operating_unit_id"] = invoices_ou.id + return lines diff --git a/account_operating_unit/models/res_company.py b/account_operating_unit/models/res_company.py new file mode 100644 index 0000000000..1700fb1f4c --- /dev/null +++ b/account_operating_unit/models/res_company.py @@ -0,0 +1,33 @@ +# © 2019 ForgeFlow S.L. +# © 2019 Serpent Consulting Services Pvt. Ltd. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class ResCompany(models.Model): + _inherit = "res.company" + + inter_ou_clearing_account_id = fields.Many2one( + comodel_name="account.account", + string="Inter-operating unit clearing account", + ) + ou_is_self_balanced = fields.Boolean( + string="Operating Units are self-balanced", + help="Activate if your company is " + "required to generate a balanced" + " balance sheet for each " + "operating unit.", + ) + + @api.constrains("ou_is_self_balanced", "inter_ou_clearing_account_id") + def _inter_ou_clearing_acc_required(self): + for rec in self: + if rec.ou_is_self_balanced and not rec.inter_ou_clearing_account_id: + raise UserError( + self.env._( + "Configuration error. Please provide an " + "Inter-operating unit clearing account." + ) + ) diff --git a/account_operating_unit/pyproject.toml b/account_operating_unit/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/account_operating_unit/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/account_operating_unit/readme/CONFIGURE.md b/account_operating_unit/readme/CONFIGURE.md new file mode 100644 index 0000000000..13df13e0ea --- /dev/null +++ b/account_operating_unit/readme/CONFIGURE.md @@ -0,0 +1,13 @@ +If your company is required to generate a balanced balance sheet by +Operating Unit you can specify at company level that Operating Units +should be self-balanced, and then indicate a self-balancing clearing +account. + +1. Create an account "Inter-OU Clearing". It is a balance sheet + account. +2. Go to *Settings / Companies / Configuration* and Set the "Operating + Units are self-balanced" checkbox. Then set the "Inter-OU Clearing" + account in "Inter-Operating Unit clearing account" field. +3. Go to *Accounting / Configuration / Accounting / Journals* and + define, for each Payment Method, the Operating Unit that will be + used in payments. diff --git a/account_operating_unit/readme/CONTRIBUTORS.md b/account_operating_unit/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..f181c5257d --- /dev/null +++ b/account_operating_unit/readme/CONTRIBUTORS.md @@ -0,0 +1,14 @@ +- ForgeFlow \<\> +- Jordi Ballester Alomar \<\> +- Aarón Henríquez \<\> +- Serpent Consulting Services Pvt. Ltd. \<\> +- WilldooIT Pty Ltd \<\> +- Michael Villamar \<\> +- Jarsa Sistemas \<\> +- Alan Ramos \<\> +- Saran Lim. \<\> +- Pimolnat Suntian \<\> +- Hieu, Vo Minh Bao \<\> +- [Heliconia Solutions Pvt. Ltd.](https://www.heliconia.io) + - Bhavesh Heliconia +- Julien Coux \<\> diff --git a/account_operating_unit/readme/DESCRIPTION.md b/account_operating_unit/readme/DESCRIPTION.md new file mode 100644 index 0000000000..ef85d9a50b --- /dev/null +++ b/account_operating_unit/readme/DESCRIPTION.md @@ -0,0 +1,24 @@ +This module allows a company to manage the accounting based on Operating +Units (OU's). + +- The financial reports (Trial Balance, P&L, Balance Sheet), allow to + report the balances of one or more OU's. +- If a company wishes to report Balance Sheet and P&L accounts based on + OU's, they should indicate at company level that the OU's are + self-balanced, and the corresponding Inter-Operating Unit clearing + account. The Chart of Accounts will always be balanced, for each + Operating Unit. +- A company considering Operating Unit as applicable to report only + profits and losses will not need to set the OU's as self-balanced. +- The self-balancing of Operating Unit is ensured at the time of posting + a journal entry. In case that the journal involves posting of items in + separate Operating Units, new journal items will be created, using the + Inter-Operating Unit clearing account, to ensure that each OU is going + to be self-balanced for that journal entry. +- Adds the Operating Unit to the invoice. A user can choose what OU to + create the invoice for. +- Adds the Operating Unit to payments and payment methods. The operating + unit of a payment will be that of the payment method chosen. +- Implements security rules at OU level to invoices, payments and + journal items. +- Adds the Operating Unit to the cash basis journal entries. diff --git a/account_operating_unit/readme/ROADMAP.md b/account_operating_unit/readme/ROADMAP.md new file mode 100644 index 0000000000..e735215e99 --- /dev/null +++ b/account_operating_unit/readme/ROADMAP.md @@ -0,0 +1,7 @@ +- The *General Ledger*, *Aged Partner Balance* reports do not support + the filter by Operating Unit. Basically due to lack of proper hooks in + the standard methods used by these reports, to introduce the ability + to filter by Operating Unit. +- Trial Balance, P&L and Balance Sheet were removed from Odoo Community. + Once OCA Financial Reports are migrated to 13 we can add the Operating + Unit to those reports. diff --git a/account_operating_unit/readme/USAGE.md b/account_operating_unit/readme/USAGE.md new file mode 100644 index 0000000000..20f339da46 --- /dev/null +++ b/account_operating_unit/readme/USAGE.md @@ -0,0 +1,12 @@ +- Add the Operating Unit to invoices. +- Report invoices by Operating Unit in *Accounting / Reporting* + *Business Intelligence / Invoices* +- Add the Default Operating Unit to account move. Then all move lines + will by default adopt this Operating Unit. +- Add Operating Units to the move lines. If they differ across lines of + the same move, and the OU's are self-balanced, then additional move + lines will be created so as to make the move self-balanced from OU + perspective. +- In the menu *Accounting / Reporting / PDF Reports*, you can indicate + the Operating Units to report on, for the *Trial Balance*, *Balance + Sheet*, *Profit and Loss*, and *Financial Reports*. diff --git a/account_operating_unit/report/__init__.py b/account_operating_unit/report/__init__.py new file mode 100644 index 0000000000..828ca7dd4d --- /dev/null +++ b/account_operating_unit/report/__init__.py @@ -0,0 +1,3 @@ +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from . import account_invoice_report diff --git a/account_operating_unit/report/account_invoice_report.py b/account_operating_unit/report/account_invoice_report.py new file mode 100644 index 0000000000..ff063c502b --- /dev/null +++ b/account_operating_unit/report/account_invoice_report.py @@ -0,0 +1,18 @@ +# © 2019 ForgeFlow S.L. +# © 2019 Serpent Consulting Services Pvt. Ltd. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import fields, models +from odoo.tools import SQL + + +class AccountInvoiceReport(models.Model): + _inherit = "account.invoice.report" + + operating_unit_id = fields.Many2one( + comodel_name="operating.unit", + string="Operating Unit", + ) + + def _select(self): + return SQL("%s, line.operating_unit_id", super()._select()) diff --git a/account_operating_unit/security/account_security.xml b/account_operating_unit/security/account_security.xml new file mode 100644 index 0000000000..57576d995b --- /dev/null +++ b/account_operating_unit/security/account_security.xml @@ -0,0 +1,71 @@ + + + + + + + + + ['|', ('operating_unit_id','=',False), + ('operating_unit_id','in',operating_unit_ids)] + + Journals from allowed operating units + + + + + + + + + + ['|', ('operating_unit_id','=',False), ('operating_unit_id','in', + operating_unit_ids)] + + Move lines from allowed operating units + + + + + + + + + + ['|', ('operating_unit_id','=',False), ('operating_unit_id','in', + operating_unit_ids)] + + Moves from allowed operating units + + + + + + + + + + ['|', ('operating_unit_id','=',False), ('operating_unit_id','in', + operating_unit_ids)] + + Payments from allowed operating units + + + + + + + + + + ['|', ('operating_unit_id','=',False), ('operating_unit_id','in', + operating_unit_ids)] + + Invoice Report from allowed operating units + + + + + + + diff --git a/account_operating_unit/static/description/icon.png b/account_operating_unit/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/account_operating_unit/static/description/icon.png differ diff --git a/account_operating_unit/static/description/index.html b/account_operating_unit/static/description/index.html new file mode 100644 index 0000000000..495611ba8e --- /dev/null +++ b/account_operating_unit/static/description/index.html @@ -0,0 +1,518 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Accounting with Operating Units

+ +

Beta License: LGPL-3 OCA/operating-unit Translate me on Weblate Try me on Runboat

+

This module allows a company to manage the accounting based on Operating +Units (OU’s).

+
    +
  • The financial reports (Trial Balance, P&L, Balance Sheet), allow to +report the balances of one or more OU’s.
  • +
  • If a company wishes to report Balance Sheet and P&L accounts based on +OU’s, they should indicate at company level that the OU’s are +self-balanced, and the corresponding Inter-Operating Unit clearing +account. The Chart of Accounts will always be balanced, for each +Operating Unit.
  • +
  • A company considering Operating Unit as applicable to report only +profits and losses will not need to set the OU’s as self-balanced.
  • +
  • The self-balancing of Operating Unit is ensured at the time of posting +a journal entry. In case that the journal involves posting of items in +separate Operating Units, new journal items will be created, using the +Inter-Operating Unit clearing account, to ensure that each OU is going +to be self-balanced for that journal entry.
  • +
  • Adds the Operating Unit to the invoice. A user can choose what OU to +create the invoice for.
  • +
  • Adds the Operating Unit to payments and payment methods. The operating +unit of a payment will be that of the payment method chosen.
  • +
  • Implements security rules at OU level to invoices, payments and +journal items.
  • +
  • Adds the Operating Unit to the cash basis journal entries.
  • +
+

Table of contents

+ +
+

Configuration

+

If your company is required to generate a balanced balance sheet by +Operating Unit you can specify at company level that Operating Units +should be self-balanced, and then indicate a self-balancing clearing +account.

+
    +
  1. Create an account “Inter-OU Clearing”. It is a balance sheet account.
  2. +
  3. Go to Settings / Companies / Configuration and Set the “Operating +Units are self-balanced” checkbox. Then set the “Inter-OU Clearing” +account in “Inter-Operating Unit clearing account” field.
  4. +
  5. Go to Accounting / Configuration / Accounting / Journals and +define, for each Payment Method, the Operating Unit that will be used +in payments.
  6. +
+
+
+

Usage

+
    +
  • Add the Operating Unit to invoices.
  • +
  • Report invoices by Operating Unit in Accounting / Reporting +Business Intelligence / Invoices
  • +
  • Add the Default Operating Unit to account move. Then all move lines +will by default adopt this Operating Unit.
  • +
  • Add Operating Units to the move lines. If they differ across lines of +the same move, and the OU’s are self-balanced, then additional move +lines will be created so as to make the move self-balanced from OU +perspective.
  • +
  • In the menu Accounting / Reporting / PDF Reports, you can indicate +the Operating Units to report on, for the Trial Balance, Balance +Sheet, Profit and Loss, and Financial Reports.
  • +
+
+
+

Known issues / Roadmap

+
    +
  • The General Ledger, Aged Partner Balance reports do not support +the filter by Operating Unit. Basically due to lack of proper hooks in +the standard methods used by these reports, to introduce the ability +to filter by Operating Unit.
  • +
  • Trial Balance, P&L and Balance Sheet were removed from Odoo Community. +Once OCA Financial Reports are migrated to 13 we can add the Operating +Unit to those reports.
  • +
+
+
+

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

+
    +
  • ForgeFlow
  • +
  • Serpent Consulting Services Pvt. Ltd.
  • +
  • WilldooIT Pty Ltd
  • +
+
+
+

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.

+

This module is part of the OCA/operating-unit project on GitHub.

+

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

+
+
+
+
+ + diff --git a/account_operating_unit/tests/__init__.py b/account_operating_unit/tests/__init__.py new file mode 100644 index 0000000000..68197dab28 --- /dev/null +++ b/account_operating_unit/tests/__init__.py @@ -0,0 +1,8 @@ +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from . import test_account_operating_unit +from . import test_invoice_operating_unit +from . import test_cross_ou_journal_entry +from . import test_operating_unit_security +from . import test_payment_operating_unit +from . import test_account_reconcile diff --git a/account_operating_unit/tests/test_account_operating_unit.py b/account_operating_unit/tests/test_account_operating_unit.py new file mode 100644 index 0000000000..b16c11e6ae --- /dev/null +++ b/account_operating_unit/tests/test_account_operating_unit.py @@ -0,0 +1,206 @@ +# © 2019 ForgeFlow S.L. +# © 2019 Serpent Consulting Services Pvt. Ltd. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo.fields import Command, Domain +from odoo.tests import tagged + +from odoo.addons.account.tests.common import AccountTestInvoicingCommon +from odoo.addons.operating_unit.tests.common import OperatingUnitCommon + + +@tagged("post_install", "-at_install") +class TestAccountOperatingUnit(AccountTestInvoicingCommon, OperatingUnitCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + + # Get the Operating Unit Manager group + cls.ou_manager_group = cls.env.ref( + "operating_unit.group_manager_operating_unit" + ) + + # Models + cls.aml_model = cls.env["account.move.line"] + cls.move_model = cls.env["account.move"] + cls.account_model = cls.env["account.account"] + cls.journal_model = cls.env["account.journal"] + cls.product_model = cls.env["product.product"] + cls.payment_model = cls.env["account.payment"] + cls.register_payments_model = cls.env["account.payment.register"] + + # Groups + cls.grp_acc_manager = cls.env.ref("account.group_account_manager") + cls.grp_acc_config = cls.env.ref("account.group_account_user") + + cls.product1 = cls.env["product.product"].create( + { + "name": "Storage Box", + "type": "consu", + "standard_price": 14.0, + "list_price": 15.8, + } + ) + cls.product2 = cls.env["product.product"].create( + { + "name": "Pedal Bin", + "type": "consu", + "standard_price": 10.0, + "list_price": 47.0, + } + ) + cls.product3 = cls.env["product.product"].create( + { + "name": "Conference Chair", + "type": "consu", + "standard_price": 28.0, + "list_price": 33.0, + } + ) + + # Add Operating Unit Manager group to env.user + cls.env.user.write( + { + "group_ids": [Command.link(cls.ou_manager_group.id)], + "company_ids": [Command.link(cls.company.id)], + "company_id": cls.company.id, + } + ) + + # Set up operating units with sudo() + cls.ou1 = cls.ou1.sudo() + cls.b2b = cls.b2b.sudo() + cls.b2c = cls.b2c.sudo() + + # Update operating units' company with sudo() + operating_units = cls.ou1 | cls.b2b | cls.b2c + operating_units.write({"company_id": cls.company.id}) + + # Setup user1 with all required groups + cls.user1.write( + { + "group_ids": [ + Command.link(cls.grp_acc_manager.id), + Command.link(cls.grp_acc_config.id), + Command.link(cls.ou_manager_group.id), + ], + "operating_unit_ids": [ + Command.link(cls.b2b.id), + Command.link(cls.b2c.id), + ], + "company_id": cls.company.id, + "company_ids": [Command.link(cls.company.id)], + } + ) + + # Create accounts + cls.current_asset_account_id = cls.account_model.create( + { + "name": "Current asset - Test", + "code": "test.current.asset", + "account_type": "asset_current", + } + ) + + cls.inter_ou_account_id = cls.account_model.create( + { + "name": "Inter-OU Clearing", + "code": "test.inter.ou", + "account_type": "equity", + } + ) + + # Assign the Inter-OU Clearing account to the company + cls.company.inter_ou_clearing_account_id = cls.inter_ou_account_id.id + cls.company.ou_is_self_balanced = True + + # Setup user2 with all required groups + cls.user2.write( + { + "group_ids": [ + Command.link(cls.grp_acc_manager.id), + Command.link(cls.grp_acc_config.id), + Command.link(cls.ou_manager_group.id), + ], + "operating_unit_ids": [Command.link(cls.b2c.id)], + "company_id": cls.company.id, + "company_ids": [Command.link(cls.company.id)], + } + ) + + # Create cash accounts + cls.cash1_account_id = cls.account_model.create( + { + "name": "Cash 1 - Test", + "code": "test.cash.1", + "account_type": "asset_current", + } + ) + + cls.cash2_account_id = cls.account_model.create( + { + "name": "Cash 2 - Test", + "code": "cash2", + "account_type": "asset_current", + } + ) + + # Create journals with proper company consistency + ou1_journal_vals = { + "name": "Cash Journal 1 - Test", + "code": "cash1", + "type": "cash", + "company_id": cls.company.id, + "default_account_id": cls.cash1_account_id.id, + "operating_unit_id": cls.ou1.id, + } + + b2b_journal_vals = { + "name": "Cash Journal 2 - Test", + "code": "test_cash_2", + "type": "cash", + "company_id": cls.company.id, + "default_account_id": cls.cash2_account_id.id, + "operating_unit_id": cls.b2b.id, + } + + cls.cash_journal_ou1 = cls.journal_model.sudo().create(ou1_journal_vals) + cls.cash2_journal_b2b = cls.journal_model.sudo().create(b2b_journal_vals) + + def _prepare_invoice(self, operating_unit_id, name="Test Supplier Invoice"): + line_products = [ + (self.product1, 1000), + (self.product2, 500), + (self.product3, 800), + ] + # Prepare invoice lines + lines = [] + for product, qty in line_products: + line_values = { + "name": product.name, + "product_id": product.id, + "quantity": qty, + "price_unit": 50, + "account_id": self.env["account.account"] + .search( + Domain.AND( + [ + Domain("account_type", "=", "expense"), + Domain("company_ids", "in", self.company.ids), + ] + ), + limit=1, + ) + .id, + # Adding this line so the taxes are explicitly excluded from the lines + "tax_ids": [], + } + lines.append(Command.create(line_values)) + inv_vals = { + "partner_id": self.partner.id, + "operating_unit_id": operating_unit_id, + "name": name, + "move_type": "in_invoice", + "invoice_line_ids": lines, + } + return inv_vals diff --git a/account_operating_unit/tests/test_account_reconcile.py b/account_operating_unit/tests/test_account_reconcile.py new file mode 100644 index 0000000000..43bac68390 --- /dev/null +++ b/account_operating_unit/tests/test_account_reconcile.py @@ -0,0 +1,257 @@ +from odoo.fields import Command + +from odoo.addons.base.tests.common import BaseCommon + + +class TestAccountBankStatementLine(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Create a partner + cls.partner = cls.env["res.partner"].create( + { + "name": "Test Partner", + "email": "test@example.com", + } + ) + + # Create an operating unit + cls.operating_unit = cls.env["operating.unit"].create( + { + "name": "Test OU", + "code": "TEST", + "partner_id": cls.partner.id, + } + ) + + # Create accounts + cls.account_receivable = cls.env["account.account"].create( + { + "name": "Test Receivable Account", + "code": "TEST1", + "account_type": "asset_receivable", + "reconcile": True, + } + ) + + cls.account_revenue = cls.env["account.account"].create( + { + "code": "X2020", + "name": "Product Sales - (test)", + "account_type": "income", + } + ) + + # Create tax accounts + cls.tax_account = cls.env["account.account"].create( + { + "name": "Tax Account", + "code": "TAX", + "account_type": "liability_current", + "reconcile": False, + } + ) + + # Create a journal + cls.journal = cls.env["account.journal"].create( + { + "name": "Test Journal", + "type": "sale", + "code": "TESTJ", + "default_account_id": cls.account_revenue.id, + } + ) + + cls.currency = cls.env["res.currency"].create( + { + "name": "C", + "symbol": "C", + "rounding": 0.01, + "currency_unit_label": "Curr", + "rate": 1, + } + ) + + # Get cash basis tax account + cls.cash_basis_base_account = cls.env["account.account"].create( + { + "name": "Cash Basis Base Account", + "code": "CBBA", + "account_type": "liability_current", + "reconcile": True, + } + ) + + # Create a tax for cash basis tests with required fields + cls.tax_line = cls.env["account.tax"].create( + { + "name": "Test Tax", + "amount": 15.0, + "amount_type": "percent", + "type_tax_use": "sale", + "tax_exigibility": "on_payment", + "cash_basis_transition_account_id": cls.cash_basis_base_account.id, + "invoice_repartition_line_ids": [ + Command.create( + { + "factor_percent": 100, + "repartition_type": "base", + }, + ), + Command.create( + { + "factor_percent": 100, + "repartition_type": "tax", + "account_id": cls.tax_account.id, + }, + ), + ], + "refund_repartition_line_ids": [ + Command.create( + { + "factor_percent": 100, + "repartition_type": "base", + }, + ), + Command.create( + { + "factor_percent": 100, + "repartition_type": "tax", + "account_id": cls.tax_account.id, + }, + ), + ], + } + ) + + analytic_plan = cls.env["account.analytic.plan"].create( + {"name": "Plan with Tax details"} + ) + cls.analytic_account = cls.env["account.analytic.account"].create( + { + "name": "Analytic account with Tax details", + "plan_id": analytic_plan.id, + "company_id": False, + } + ) + + # Create analytic distribution + cls.analytic_distribution = {str(cls.analytic_account.id): 100} + + # Create a move with balanced entries + cls.move = cls.env["account.move"].create( + { + "name": "Test Move", + "move_type": "entry", + "journal_id": cls.journal.id, + "line_ids": [ + Command.create( + { + "name": "Debit Line", + "account_id": cls.account_receivable.id, + "operating_unit_id": cls.operating_unit.id, + "partner_id": cls.partner.id, + "debit": 100.0, + "credit": 0.0, + "amount_currency": 100.0, + "currency_id": cls.env.company.currency_id.id, + "analytic_distribution": cls.analytic_distribution, + }, + ), + Command.create( + { + "name": "Credit Line", + "account_id": cls.account_revenue.id, + "operating_unit_id": cls.operating_unit.id, + "partner_id": cls.partner.id, + "debit": 0.0, + "credit": 100.0, + "amount_currency": -100.0, + "currency_id": cls.env.company.currency_id.id, + "analytic_distribution": cls.analytic_distribution, + }, + ), + ], + } + ) + + def test_prepare_cash_basis_base_line_vals(self): + """Test operating unit propagation in cash basis base line""" + reconcile = self.env["account.partial.reconcile"] + move_line = self.move.line_ids.filtered(lambda x: x.debit > 0) + result = reconcile._prepare_cash_basis_base_line_vals(move_line, 100.0, 100.0) + + self.assertEqual( + result["operating_unit_id"], + self.operating_unit.id, + "Operating unit not correctly set on cash basis base line", + ) + + def test_prepare_cash_basis_counterpart_base_line_vals(self): + """Test operating unit propagation in cash basis counterpart base line""" + reconcile = self.env["account.partial.reconcile"] + base_vals = { + "operating_unit_id": self.operating_unit.id, + "name": "Test", + "debit": 100.0, + "credit": 0.0, + "account_id": self.account_receivable.id, + "partner_id": self.partner.id, + "amount_currency": 100.0, + "currency_id": self.env.company.currency_id.id, + "analytic_distribution": self.analytic_distribution, + "display_type": "cogs", + } + + result = reconcile._prepare_cash_basis_counterpart_base_line_vals(base_vals) + + self.assertEqual( + result["operating_unit_id"], + self.operating_unit.id, + "Operating unit not correctly set on cash basis counterpart base line", + ) + + def test_prepare_cash_basis_tax_line_vals(self): + """Test operating unit propagation in cash basis tax line""" + reconcile = self.env["account.partial.reconcile"] + move_line = self.move.line_ids.filtered(lambda x: x.debit > 0) + result = reconcile._prepare_cash_basis_tax_line_vals(move_line, 15.0, 15.0) + + self.assertEqual( + result["operating_unit_id"], + self.operating_unit.id, + "Operating unit not correctly set on cash basis tax line", + ) + + def test_prepare_cash_basis_counterpart_tax_line_vals(self): + """Test operating unit propagation in cash basis counterpart tax line""" + reconcile = self.env["account.partial.reconcile"] + tax_repartition_line = self.tax_line.invoice_repartition_line_ids.filtered( + lambda x: x.repartition_type == "tax" + ) + + # Ensure all required keys are present in tax_vals + tax_vals = { + "operating_unit_id": self.operating_unit.id, + "name": "Test Tax", + "debit": 15.0, + "credit": 0.0, + "account_id": self.tax_account.id, + "partner_id": self.partner.id, + "tax_repartition_line_id": tax_repartition_line.id, + "analytic_distribution": self.analytic_distribution, + "amount_currency": 15.0, # Add the missing key + "currency_id": self.currency.id, # Ensure currency_id is set + "display_type": "tax", + } + # Call the method under test + result = reconcile._prepare_cash_basis_counterpart_tax_line_vals( + tax_repartition_line, tax_vals + ) + + # Verify the expected result + self.assertEqual( + result["operating_unit_id"], + self.operating_unit.id, + "Operating unit not correctly set on cash basis counterpart tax line", + ) diff --git a/account_operating_unit/tests/test_cross_ou_journal_entry.py b/account_operating_unit/tests/test_cross_ou_journal_entry.py new file mode 100644 index 0000000000..5fc88184fd --- /dev/null +++ b/account_operating_unit/tests/test_cross_ou_journal_entry.py @@ -0,0 +1,118 @@ +# © 2019 ForgeFlow S.L. +# © 2019 Serpent Consulting Services Pvt. Ltd. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo.exceptions import UserError +from odoo.fields import Command, Domain +from odoo.tests import Form, tagged + +from . import test_account_operating_unit as test_ou + + +@tagged("post_install", "-at_install") +class TestCrossOuJournalEntry(test_ou.TestAccountOperatingUnit): + def _check_balance(self, account_id, acc_type="clearing"): + # Check balance for all operating units + domain = Domain("account_id", "=", account_id) + balance = self._get_balance(domain) + self.assertEqual(balance, 0.0, "Balance is 0 for all Operating Units.") + # Check balance for operating B2B units + domain = Domain.AND( + [ + Domain("account_id", "=", account_id), + Domain("operating_unit_id", "=", self.b2b.id), + ] + ) + balance = self._get_balance(domain) + if acc_type == "other": + self.assertEqual(balance, -100, "Balance is -100 for Operating Unit B2B.") + else: + self.assertEqual(balance, 100, "Balance is 100 for Operating Unit B2B.") + # Check balance for operating B2C units + domain = Domain.AND( + [ + Domain("account_id", "=", account_id), + Domain("operating_unit_id", "=", self.b2c.id), + ] + ) + balance = self._get_balance(domain) + if acc_type == "other": + self.assertEqual(balance, 100.0, "Balance is 100 for Operating Unit B2C.") + else: + self.assertEqual(balance, -100.0, "Balance is -100 for Operating Unit B2C.") + + def _get_balance(self, domain): + """ + Call _read_group method and return the balance of particular account. + """ + aml_rec = self.aml_model.with_user(self.user1.id)._read_group( + domain, ["account_id"], ["debit:sum", "credit:sum"] + )[0] + return aml_rec[1] - aml_rec[2] + + def test_cross_ou_journal_entry(self): + """Test balance of cross OU journal entries. + Test that when I create a manual journal entry with multiple + operating units, new cross-operating unit entries are created + automatically whent the journal entry is posted, ensuring that each + OU is self-balanced.""" + # Create Journal Entries and check the balance of the account + # based on different operating units. + self.company.write( + {"inter_ou_clearing_account_id": self.inter_ou_account_id.id} + ) + # Create Journal Entries + journal_ids = self.journal_model.search( + Domain.AND( + [ + Domain("code", "=", "MISC"), + Domain("company_id", "=", self.company.id), + ] + ), + limit=1, + ) + # get default values of account move + move_vals = self.move_model.default_get([]) + lines = [ + Command.create( + { + "name": "Test", + "account_id": self.current_asset_account_id.id, + "debit": 0, + "credit": 100, + "operating_unit_id": self.b2b.id, + }, + ), + Command.create( + { + "name": "Test", + "account_id": self.current_asset_account_id.id, + "debit": 100, + "credit": 0, + "operating_unit_id": self.b2c.id, + }, + ), + ] + move_vals.update( + {"journal_id": journal_ids and journal_ids.id, "line_ids": lines} + ) + move = ( + self.move_model.with_user(self.user1.id) + .with_context(check_move_validity=False) + .create(move_vals) + ) + # Post journal entries + move.action_post() + # Check the balance of the account + self._check_balance(self.current_asset_account_id.id, acc_type="other") + clearing_account_id = self.company.inter_ou_clearing_account_id.id + self._check_balance(clearing_account_id, acc_type="clearing") + + def test_journal_no_ou(self): + """Test journal can not create if use self-balance but not ou in journal""" + with self.assertRaises(UserError): + with Form(self.journal_model.with_company(self.company)) as f: + f.type = "bank" + f.name = "Test new bank not ou" + f.code = "testcode" + f.save() diff --git a/account_operating_unit/tests/test_invoice_operating_unit.py b/account_operating_unit/tests/test_invoice_operating_unit.py new file mode 100644 index 0000000000..e54f202440 --- /dev/null +++ b/account_operating_unit/tests/test_invoice_operating_unit.py @@ -0,0 +1,83 @@ +# © 2019 ForgeFlow S.L. +# © 2019 Serpent Consulting Services Pvt. Ltd. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo.exceptions import UserError +from odoo.fields import Command, Domain +from odoo.tests import Form, tagged + +from . import test_account_operating_unit as test_ou + + +@tagged("post_install", "-at_install") +class TestInvoiceOperatingUnit(test_ou.TestAccountOperatingUnit): + def test_create_invoice_validate(self): + """Create & Validate the invoice. + Test that when an invoice is created, the operating unit is + passed to the accounting journal items. + """ + # Create invoice + self.invoice = self.move_model.with_user(self.user1.id).create( + self._prepare_invoice(self.b2b.id) + ) + self.invoice.invoice_date = self.invoice.date + # Validate the invoice + self.invoice.with_user(self.user1.id).action_post() + # Check Operating Units in journal entries + all_op_units = all( + move_line.operating_unit_id.id == self.b2b.id + for move_line in self.invoice.line_ids + ) + # Assert if journal entries of the invoice + # have different operating units + self.assertNotEqual( + all_op_units, + False, + "Journal Entries have different Operating Units.", + ) + # Test change ou in move + with self.assertRaises(UserError): + self.invoice.line_ids[0].operating_unit_id = self.b2c.id + # Test change company in move + new_company = self.env["res.company"].create({"name": "New Company"}) + with self.assertRaises(UserError): + self.invoice.line_ids[0].company_id = new_company.id + # Check report invoice + self.env["account.invoice.report"].sudo()._read_group( + Domain.TRUE, ["operating_unit_id"], ["operating_unit_id:array_agg"] + ) + + def test_form(self): + """Test that the UI behaves as expected""" + journal_b2b = self.env["account.journal"].create( + { + "name": "B2B journal", + "code": "B2B", + "type": "sale", + "operating_unit_id": self.b2b.id, + "company_id": self.company.id, + } + ) + + # Ensure user has the correct operating unit + self.user1.write({"operating_unit_ids": [Command.link(self.ou1.id)]}) + + # Open the form with default operating unit context + with Form( + self.env["account.move"].with_context( + default_operating_unit_id=self.ou1.id, default_move_type="out_invoice" + ) + ) as invoice_form: + # Check the default operating unit in the form + self.assertEqual(invoice_form.operating_unit_id, self.ou1) + + # Copy OU1 journal and validate operating unit + ou1_journal = invoice_form.journal_id.copy( + {"operating_unit_id": self.ou1.id, "company_id": self.company.id} + ) + # Update operating unit and journal in the form + invoice_form.operating_unit_id = self.b2b + self.assertEqual(invoice_form.journal_id, journal_b2b) + + invoice_form.journal_id = ou1_journal + self.assertEqual(invoice_form.operating_unit_id, self.ou1) diff --git a/account_operating_unit/tests/test_operating_unit_security.py b/account_operating_unit/tests/test_operating_unit_security.py new file mode 100644 index 0000000000..4e70691781 --- /dev/null +++ b/account_operating_unit/tests/test_operating_unit_security.py @@ -0,0 +1,22 @@ +# © 2019 ForgeFlow S.L. +# © 2019 Serpent Consulting Services Pvt. Ltd. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo.fields import Domain +from odoo.tests import tagged + +from . import test_account_operating_unit as test_ou + + +@tagged("post_install", "-at_install") +class TestOuSecurity(test_ou.TestAccountOperatingUnit): + def test_security(self): + """Test Security of Account Operating Unit""" + # User 2 is only assigned to Operating Unit B2C, and cannot list + # Journal Entries from Operating Unit B2B. + move_ids = self.aml_model.with_user(self.user2.id).search( + Domain("operating_unit_id", "=", self.b2b.id) + ) + self.assertFalse( + move_ids, f"user_2 should not have access to OU {self.b2b.name}" + ) diff --git a/account_operating_unit/tests/test_payment_operating_unit.py b/account_operating_unit/tests/test_payment_operating_unit.py new file mode 100644 index 0000000000..9f556df05d --- /dev/null +++ b/account_operating_unit/tests/test_payment_operating_unit.py @@ -0,0 +1,105 @@ +# © 2019 ForgeFlow S.L. +# © 2019 Serpent Consulting Services Pvt. Ltd. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +import time + +from odoo.fields import Domain +from odoo.tests import tagged + +from . import test_account_operating_unit as test_ou + + +@tagged("post_install", "-at_install") +class TestInvoiceOperatingUnit(test_ou.TestAccountOperatingUnit): + def test_payment_from_invoice(self): + """Create and invoice and a subsquent payment, in another OU""" + + # Create invoice for B2B operating unit + self.invoice = self.move_model.with_user(self.user1.id).create( + self._prepare_invoice(self.b2b.id) + ) + self.invoice.invoice_date = self.invoice.date + # Validate the invoice + self.invoice.with_user(self.user1.id).action_post() + + # Pay the invoice using a cash journal associated to the main company + ctx = {"active_model": "account.move", "active_ids": [self.invoice.id]} + payment_method_id = self.cash_journal_ou1.outbound_payment_method_line_ids[0] + register_payments = self.register_payments_model.with_context(**ctx).create( + { + "payment_date": time.strftime("%Y") + "-07-15", + "journal_id": self.cash_journal_ou1.id, + "payment_method_line_id": payment_method_id.id, + } + ) + + register_payments.action_create_payments() + payment = self.payment_model.search(Domain.TRUE, order="id desc", limit=1) + # Validate that inter OU balance move lines are created + self.assertEqual(len(payment.move_id.line_ids), 4) + self.assertAlmostEqual(payment.amount, self.invoice.amount_total) + self.assertIn(payment.state, ["paid", "in_process"]) + self.assertEqual(self.invoice.payment_state, "paid") + + def test_payment_from_two_invoices(self): + """Create two invoices of different OU and payment from a third OU""" + + # Create invoices for B2B and B2C operating units + to_create = [ + self._prepare_invoice(self.b2b.id, "SUPP/B2B/01"), + self._prepare_invoice(self.b2c.id, "SUPP/B2C/02"), + ] + invoices = self.move_model.with_user(self.user1.id).create(to_create) + for invoice in invoices: + invoice.invoice_date = invoice.date + # Validate the invoices + invoices.with_user(self.user1.id).action_post() + + # Pay the invoices using a cash journal associated to the main company + payment_method_id = self.cash_journal_ou1.outbound_payment_method_line_ids[0] + self.register_payments_model.with_context( + active_model="account.move", active_ids=invoices.ids + ).create( + { + "payment_date": time.strftime("%Y") + "-07-15", + "journal_id": self.cash_journal_ou1.id, + "payment_method_line_id": payment_method_id.id, + } + )._create_payments() + + payments = self.payment_model.search(Domain.TRUE, order="id desc", limit=2) + inter_ou_moves = self.move_model.search( + Domain("ref", "=", "Inter OU Balancing"), order="id desc", limit=2 + ) + self.assertEqual(sum(inter_ou_moves[0].mapped("line_ids.debit")), 115000) + self.assertEqual(sum(inter_ou_moves[1].mapped("line_ids.debit")), 115000) + for payment in payments: + # Validate that inter OU balance move lines are created + self.assertEqual(len(payment.move_id.line_ids), 2) + self.assertEqual(payment.amount, invoices[0].amount_total) + self.assertIn(payment.state, ["paid", "in_process"]) + for invoice in invoices: + self.assertEqual(invoice.payment_state, "paid") + + def test_payment_transfer(self): + """Create a transfer payment with journals in different OU""" + payment_method_id = self.cash_journal_ou1.outbound_payment_method_line_ids[0] + payment = self.payment_model.create( + { + "payment_type": "outbound", + "amount": 115000, + "date": time.strftime("%Y") + "-07-15", + "journal_id": self.cash_journal_ou1.id, + "payment_method_line_id": payment_method_id.id, + } + ) + payment.action_post() + payments = payment + payment.paired_internal_transfer_payment_id + self.assertEqual(len(payments.move_id.mapped("line_ids.operating_unit_id")), 1) + # Validate that every move has their correct OU + for move in payments.move_id: + ou_in_lines = move.line_ids.operating_unit_id + self.assertEqual(len(ou_in_lines), 1) + ou_in_journal = move.journal_id.operating_unit_id + self.assertEqual(ou_in_lines, ou_in_journal) diff --git a/account_operating_unit/views/account_invoice_report_view.xml b/account_operating_unit/views/account_invoice_report_view.xml new file mode 100644 index 0000000000..b60944dfc9 --- /dev/null +++ b/account_operating_unit/views/account_invoice_report_view.xml @@ -0,0 +1,27 @@ + + + + + + + account.invoice.report.search + account.invoice.report + + + + + + + + + + + diff --git a/account_operating_unit/views/account_journal_view.xml b/account_operating_unit/views/account_journal_view.xml new file mode 100644 index 0000000000..e000b2887d --- /dev/null +++ b/account_operating_unit/views/account_journal_view.xml @@ -0,0 +1,20 @@ + + + + + + + account.journal.form + account.journal + + + + + + + + diff --git a/account_operating_unit/views/account_move_view.xml b/account_operating_unit/views/account_move_view.xml new file mode 100644 index 0000000000..e3d4c820b0 --- /dev/null +++ b/account_operating_unit/views/account_move_view.xml @@ -0,0 +1,158 @@ + + + + + + + account.move.line.form + account.move.line + + + + + + + + + account.move.line.tree + account.move.line + + + + + + + + + Journal Items + account.move.line + + + + + + + + + + + + account.move.form + account.move + + + + + + + + + { + 'operating_unit_id': operating_unit_id, + 'default_operating_unit_id': operating_unit_id + } + + + + + { + 'default_operating_unit_id': operating_unit_id + } + + + + + + + + + + + + + account.invoice.tree + account.move + + + + + + + + + account.invoice.select + account.move + + + + + + + + + + + diff --git a/account_operating_unit/views/account_payment_view.xml b/account_operating_unit/views/account_payment_view.xml new file mode 100644 index 0000000000..209ec56ec7 --- /dev/null +++ b/account_operating_unit/views/account_payment_view.xml @@ -0,0 +1,55 @@ + + + + + + + account.payment.tree + account.payment + + + + + + + + + account.payment.search + account.payment + + + + + + + + + + account.payment.form + account.payment + + + + + + + + diff --git a/account_operating_unit/views/company_view.xml b/account_operating_unit/views/company_view.xml new file mode 100644 index 0000000000..6ea7bc9ee5 --- /dev/null +++ b/account_operating_unit/views/company_view.xml @@ -0,0 +1,19 @@ + + + + + + + res.company.form + res.company + + + + + + + + + + + diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000000..0b1364eaa4 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +odoo-addon-base_view_inheritance_extension @ git+https://github.com/OCA/server-tools.git@refs/pull/3382/head#subdirectory=base_view_inheritance_extension