diff --git a/account_operating_unit/README.rst b/account_operating_unit/README.rst new file mode 100644 index 0000000000..cecfd717fe --- /dev/null +++ b/account_operating_unit/README.rst @@ -0,0 +1,147 @@ +=============================== +Accounting with Operating Units +=============================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-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/15.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-15-0/operating-unit-15-0-account_operating_unit + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/213/15.0 + :alt: Try me on Runbot + +|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. + +#. Create an account "Inter-OU Clearing". It is a balance sheet account. +#. 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. +#. 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 smashing 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 + +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..e3c4fddcdc --- /dev/null +++ b/account_operating_unit/__init__.py @@ -0,0 +1,5 @@ +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from . import models +from . import report +from . import wizards diff --git a/account_operating_unit/__manifest__.py b/account_operating_unit/__manifest__.py new file mode 100644 index 0000000000..e87e9873e1 --- /dev/null +++ b/account_operating_unit/__manifest__.py @@ -0,0 +1,25 @@ +# © 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": "16.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"], + "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..1b736e55d0 --- /dev/null +++ b/account_operating_unit/i18n/account_operating_unit.pot @@ -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 15.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 +#: 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 "" + +#. module: account_operating_unit +#: code:addons/account_operating_unit/models/res_company.py:0 +#, python-format +msgid "" +"Configuration error. Please provide an Inter-operating unit clearing " +"account." +msgstr "" + +#. module: account_operating_unit +#: code:addons/account_operating_unit/models/account_move.py:0 +#, python-format +msgid "" +"Configuration error. The Company in the Move Line and in the Operating Unit " +"must be the same." +msgstr "" + +#. module: account_operating_unit +#: 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 "" + +#. module: account_operating_unit +#: 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 "" + +#. module: account_operating_unit +#: 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 "" + +#. 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 +#: code:addons/account_operating_unit/models/account_move.py:0 +#, python-format +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 +#: model:ir.model,name:account_operating_unit.model_account_payment_register +msgid "Register Payment" +msgstr "" + +#. module: account_operating_unit +#: code:addons/account_operating_unit/models/account_move.py:0 +#, python-format +msgid "The Company in the Move and in Operating Unit must be the same." +msgstr "" + +#. module: account_operating_unit +#: code:addons/account_operating_unit/wizards/account_payment_register.py:0 +#, python-format +msgid "The OU in the Bills/Invoices to register payment must be the same." +msgstr "" + +#. module: account_operating_unit +#: 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 "" + +#. 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..c6df2bb44c --- /dev/null +++ b/account_operating_unit/i18n/es_MX.po @@ -0,0 +1,199 @@ +# 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 +#: 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 +#: 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 +#: code:addons/account_operating_unit/models/account_move.py:0 +#, 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." + +#. module: account_operating_unit +#: 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 +#: 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 +#: 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 +#: 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 +#: model:ir.model,name:account_operating_unit.model_account_payment_register +msgid "Register Payment" +msgstr "Registrar pago" + +#. module: account_operating_unit +#: code:addons/account_operating_unit/models/account_move.py:0 +#, 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." + +#. module: account_operating_unit +#: code:addons/account_operating_unit/wizards/account_payment_register.py:0 +#, 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." + +#. module: account_operating_unit +#: 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." 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..58b83d84ae --- /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 api, models + + +class AccountBankStatementLine(models.Model): + _inherit = "account.bank.statement.line" + + @api.model + def _prepare_liquidity_move_line_vals(self): + res = super()._prepare_liquidity_move_line_vals() + res["operating_unit_id"] = self.statement_id.journal_id.operating_unit_id.id + return res + + @api.model + def _prepare_counterpart_move_line_vals(self, counterpart_vals, move_line=None): + res = super()._prepare_counterpart_move_line_vals(counterpart_vals, move_line) + res["operating_unit_id"] = self.statement_id.journal_id.operating_unit_id.id + return res diff --git a/account_operating_unit/models/account_journal.py b/account_operating_unit/models/account_journal.py new file mode 100644 index 0000000000..203d5b5b17 --- /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( + 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( + _( + "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..88b55a49aa --- /dev/null +++ b/account_operating_unit/models/account_move.py @@ -0,0 +1,237 @@ +# © 2019 ForgeFlow S.L. +# © 2019 Serpent Consulting Services Pvt. Ltd. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from contextlib import contextmanager + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + + operating_unit_id = fields.Many2one( + 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.model + def _query_get(self, domain=None): + if domain is None: + domain = [] + if self._context.get("operating_unit_ids", False): + domain.append( + ("operating_unit_id", "in", self._context.get("operating_unit_ids")) + ) + return super()._query_get(domain) + + @api.constrains("operating_unit_id", "company_id") + def _check_company_operating_unit(self): + for rec in self: + if ( + rec.company_id + and rec.operating_unit_id + and rec.company_id != rec.operating_unit_id.company_id + ): + raise UserError( + _( + "Configuration error. The Company in the" + " Move Line and in the Operating Unit must " + "be the same." + ) + ) + + @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( + _( + "Configuration error. The Operating Unit in" + " the Move Line and in the Move must be the" + " same." + ) + ) + + +class AccountMove(models.Model): + _inherit = "account.move" + + @api.model + def _default_operating_unit_id(self): + if ( + self._context.get("default_move_type", False) + and self._context.get("default_move_type") != "entry" + ): + return self.env["res.users"].operating_unit_default_get() + return False + + operating_unit_id = fields.Many2one( + comodel_name="operating.unit", + default=_default_operating_unit_id, + help="This operating unit will be defaulted in the move lines.", + readonly=True, + states={"draft": [("readonly", False)]}, + ) + + @api.onchange("invoice_line_ids") + def _onchange_invoice_line_ids(self): + if self.operating_unit_id: + for line in self.line_ids: + line.operating_unit_id = self.operating_unit_id + + @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( + [("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.onchange("journal_id") + def _onchange_journal(self): + if ( + self.journal_id + and self.journal_id.operating_unit_id + and self.journal_id.operating_unit_id != self.operating_unit_id + ): + self.operating_unit_id = self.journal_id.operating_unit_id + for line in self.line_ids: + line.operating_unit_id = self.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( + _( + "Configuration error. You need to define an" + "inter-operating unit clearing account in the " + "company settings" + ) + ) + + res = { + "name": _("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: + 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 = [] + 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: + amls.append(ml_obj.with_context(check_move_validity=True).create(line_data)) + if amls: + move.with_context(check_move_validity=False).write( + {"line_ids": [(4, 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( + _( + "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( + _("The OU in the Move and in Journal must be the same.") + ) + return True + + @api.constrains("operating_unit_id", "company_id") + def _check_company_operating_unit(self): + for move in self: + if ( + move.company_id + and move.operating_unit_id + and move.company_id != move.operating_unit_id.company_id + ): + raise UserError( + _( + "The Company in the Move and in " + "Operating Unit 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..865a1422a0 --- /dev/null +++ b/account_operating_unit/models/account_partial_reconcile.py @@ -0,0 +1,32 @@ +# 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_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": tax_line.operating_unit_id.id}) + 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..56c420cf94 --- /dev/null +++ b/account_operating_unit/models/account_payment.py @@ -0,0 +1,40 @@ +# © 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( + 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): + lines = super()._prepare_move_line_default_vals(write_off_line_vals) + for line in lines: + line["operating_unit_id"] = self.operating_unit_id.id + active_model = self._context.get("active_model", False) + if not active_model or active_model != "account.move": + return lines + invoices = self.env[self._context.get("active_model")].browse( + self._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..d2b4cd123f --- /dev/null +++ b/account_operating_unit/models/res_company.py @@ -0,0 +1,34 @@ +# © 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.tools.translate import _ + + +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( + _( + "Configuration error. Please provide an " + "Inter-operating unit clearing account." + ) + ) diff --git a/account_operating_unit/readme/CONFIGURE.rst b/account_operating_unit/readme/CONFIGURE.rst new file mode 100644 index 0000000000..3f9e463c4a --- /dev/null +++ b/account_operating_unit/readme/CONFIGURE.rst @@ -0,0 +1,10 @@ +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. + +#. Create an account "Inter-OU Clearing". It is a balance sheet account. +#. 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. +#. 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.rst b/account_operating_unit/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..7e6c69495f --- /dev/null +++ b/account_operating_unit/readme/CONTRIBUTORS.rst @@ -0,0 +1,12 @@ +* 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 +* Alejandro Leonard diff --git a/account_operating_unit/readme/DESCRIPTION.rst b/account_operating_unit/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..e8942d9e7c --- /dev/null +++ b/account_operating_unit/readme/DESCRIPTION.rst @@ -0,0 +1,23 @@ +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.rst b/account_operating_unit/readme/ROADMAP.rst new file mode 100644 index 0000000000..390531475c --- /dev/null +++ b/account_operating_unit/readme/ROADMAP.rst @@ -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.rst b/account_operating_unit/readme/USAGE.rst new file mode 100644 index 0000000000..220b3b595f --- /dev/null +++ b/account_operating_unit/readme/USAGE.rst @@ -0,0 +1,11 @@ +* 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..87547bfcf1 --- /dev/null +++ b/account_operating_unit/report/account_invoice_report.py @@ -0,0 +1,28 @@ +# © 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 + + +class AccountInvoiceReport(models.Model): + _inherit = "account.invoice.report" + + operating_unit_id = fields.Many2one( + comodel_name="operating.unit", + string="Operating Unit", + ) + + def _select(self): + select_str = super()._select() + select_str += """ + ,line.operating_unit_id + """ + return select_str + + def _group_by(self): + group_by_str = super()._group_by() + group_by_str += """ + ,line.operating_unit_id + """ + return group_by_str diff --git a/account_operating_unit/security/account_security.xml b/account_operating_unit/security/account_security.xml new file mode 100644 index 0000000000..fde883d503 --- /dev/null +++ b/account_operating_unit/security/account_security.xml @@ -0,0 +1,71 @@ + + + + + + + + + ['|', ('operating_unit_id','=',False), + ('operating_unit_id','in',user.operating_unit_ids.ids)] + + Journals from allowed operating units + + + + + + + + + + ['|', ('operating_unit_id','=',False), ('operating_unit_id','in', + user.operating_unit_ids.ids)] + + Move lines from allowed operating units + + + + + + + + + + ['|', ('operating_unit_id','=',False), ('operating_unit_id','in', + user.operating_unit_ids.ids)] + + Moves from allowed operating units + + + + + + + + + + ['|', ('operating_unit_id','=',False), ('operating_unit_id','in', + user.operating_unit_ids.ids)] + + Payments from allowed operating units + + + + + + + + + + ['|', ('operating_unit_id','=',False), ('operating_unit_id','in', + user.operating_unit_ids.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..37c721a551 --- /dev/null +++ b/account_operating_unit/static/description/index.html @@ -0,0 +1,499 @@ + + + + + + +Accounting with Operating Units + + + +
+

Accounting with Operating Units

+ + +

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

+

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 smashing 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..071cbac008 --- /dev/null +++ b/account_operating_unit/tests/__init__.py @@ -0,0 +1,7 @@ +# 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 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..6b5e75d91d --- /dev/null +++ b/account_operating_unit/tests/test_account_operating_unit.py @@ -0,0 +1,187 @@ +# © 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.tests import tagged + +from odoo.addons.account.tests.common import AccountTestInvoicingCommon + + +@tagged("post_install", "-at_install") +class TestAccountOperatingUnit(AccountTestInvoicingCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.res_users_model = cls.env["res.users"] + 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"] + + # company + cls.company = cls.env.user.company_id + cls.grp_acc_manager = cls.env.ref("account.group_account_manager") + # Main Operating Unit + cls.ou1 = cls.env.ref("operating_unit.main_operating_unit") + # B2B Operating Unit + cls.b2b = cls.env.ref("operating_unit.b2b_operating_unit") + # B2C Operating Unit + cls.b2c = cls.env.ref("operating_unit.b2c_operating_unit") + # Assign user to main company to allow to write OU + cls.env.user.write( + { + "company_ids": [(4, cls.env.ref("base.main_company").id)], + "operating_unit_ids": [ + (4, cls.b2b.id), + (4, cls.b2c.id), + ], + } + ) + # Assign company to OU + (cls.ou1 + cls.b2b + cls.b2c).write({"company_id": cls.company.id}) + # Partner + cls.partner1 = cls.env.ref("base.res_partner_1") + # Products + cls.product1 = cls.env.ref("product.product_product_7") + cls.product2 = cls.env.ref("product.product_product_9") + cls.product3 = cls.env.ref("product.product_product_11") + + # Payment methods + cls.payment_method_manual_in = cls.env.ref( + "account.account_payment_method_manual_in" + ) + + # Create user1 + cls.user_id = cls.res_users_model.with_context(no_reset_password=True).create( + { + "name": "Test Account User", + "login": "user_1", + "password": "demo", + "email": "example@yourcompany.com", + "company_id": cls.company.id, + "company_ids": [(4, cls.company.id)], + "operating_unit_ids": [(4, cls.b2b.id), (4, cls.b2c.id)], + "groups_id": [(6, 0, [cls.grp_acc_manager.id])], + } + ) + # Create cash - test account + # user_type = cls.env.ref("account.data_account_type_current_assets") + cls.current_asset_account_id = cls.account_model.create( + { + "name": "Current asset - Test", + "code": "TEST", + "account_type": "asset_current", + "company_id": cls.company.id, + } + ) + # Create Inter-OU Clearing - test account + # user_type = cls.env.ref("account.data_account_type_equity") + cls.inter_ou_account_id = cls.account_model.create( + { + "name": "Inter-OU Clearing", + "code": "inter", + # "user_type_id": user_type.id, + "account_type": "equity", + "company_id": cls.company.id, + } + ) + # 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 + + # Create user2 + cls.user2_id = cls.res_users_model.with_context(no_reset_password=True).create( + { + "name": "Test Account User", + "login": "user_2", + "password": "demo", + "email": "example@yourcompany.com", + "company_id": cls.company.id, + "company_ids": [(4, cls.company.id)], + "operating_unit_ids": [(4, cls.b2c.id)], + "groups_id": [(6, 0, [cls.grp_acc_manager.id])], + } + ) + + # Create a cash account 1 + # user_type = cls.env.ref("account.data_account_type_liquidity") + cls.cash1_account_id = cls.account_model.create( + { + "name": "Cash 1 - Test", + "code": "testcash1", + # "user_type_id": user_type.id, + "account_type": "asset_cash", + "company_id": cls.company.id, + } + ) + + # Create a journal for cash account 1, associated to the main + # operating unit + cls.cash_journal_ou1 = cls.journal_model.create( + { + "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, + } + ) + # Create a cash account 2 + # user_type = cls.env.ref("account.data_account_type_liquidity") + cls.cash2_account_id = cls.account_model.create( + { + "name": "Cash 2 - Test", + "code": "cash2", + "account_type": "liability_payable", + "company_id": cls.company.id, + } + ) + + # Create a journal for cash account 2, associated to the operating + # unit B2B + cls.cash2_journal_b2b = cls.journal_model.create( + { + "name": "Cash Journal 2 - Test", + "code": "testcash2", + "type": "cash", + "company_id": cls.company.id, + "default_account_id": cls.cash2_account_id.id, + "operating_unit_id": cls.b2b.id, + } + ) + + 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 = [] + # acc_type = self.env.ref("account.data_account_type_expenses") + + 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([("account_type", "=", "expense")], limit=1) + .id, + # Adding this line so the taxes are explicitly excluded from the lines + "tax_ids": [], + } + lines.append((0, 0, line_values)) + inv_vals = { + "partner_id": self.partner1.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_cross_ou_journal_entry.py b/account_operating_unit/tests/test_cross_ou_journal_entry.py new file mode 100644 index 0000000000..2ce2fdb1a7 --- /dev/null +++ b/account_operating_unit/tests/test_cross_ou_journal_entry.py @@ -0,0 +1,116 @@ +# © 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.tests import tagged +from odoo.tests.common import Form + +from . import test_account_operating_unit as test_ou + + +@tagged("post_install", "-at_install") +class TestCrossOuJournalEntry(test_ou.TestAccountOperatingUnit): + @classmethod + def setUpClass(cls): + super().setUpClass() + + def _check_balance(self, account_id, acc_type="clearing"): + # Check balance for all operating units + 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 = [ + ("account_id", "=", account_id), + ("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 = [ + ("account_id", "=", account_id), + ("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.user_id.id).read_group( + domain, ["debit", "credit", "account_id"], ["account_id"] + )[0] + return aml_rec.get("debit", 0) - aml_rec.get("credit", 0) + + 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( + [("code", "=", "MISC"), ("company_id", "=", self.company.id)], limit=1 + ) + # get default values of account move + move_vals = self.move_model.default_get([]) + lines = [ + ( + 0, + 0, + { + "name": "Test", + "account_id": self.current_asset_account_id.id, + "debit": 0, + "credit": 100, + "operating_unit_id": self.b2b.id, + }, + ), + ( + 0, + 0, + { + "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.user_id.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) as f: + f.type = "bank" + f.name = "Test new bank not ou" + f.code = "bankcode" + 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..dc8d7be33d --- /dev/null +++ b/account_operating_unit/tests/test_invoice_operating_unit.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.exceptions import UserError +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_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.user_id.id).create( + self._prepare_invoice(self.b2b.id) + ) + self.invoice.invoice_date = self.invoice.date + # Validate the invoice + self.invoice.with_user(self.user_id.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().search_read([]) 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..e523b8c872 --- /dev/null +++ b/account_operating_unit/tests/test_operating_unit_security.py @@ -0,0 +1,21 @@ +# © 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.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.id).search( + [("operating_unit_id", "=", self.b2b.id)] + ) + self.assertFalse( + move_ids, "user_2 should not have access to OU %s" % 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..a3e4248f2f --- /dev/null +++ b/account_operating_unit/tests/test_payment_operating_unit.py @@ -0,0 +1,99 @@ +# © 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.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.user_id.id).create( + self._prepare_invoice(self.b2b.id) + ) + self.invoice.invoice_date = self.invoice.date + # Validate the invoice + self.invoice.with_user(self.user_id.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]} + 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": self.payment_method_manual_in.id, + } + ) + + register_payments.action_create_payments() + payment = self.payment_model.search([], 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, 115000) + self.assertEqual(payment.state, "posted") + 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.user_id.id).create(to_create) + for invoice in invoices: + invoice.invoice_date = invoice.date + # Validate the invoices + invoices.with_user(self.user_id.id).action_post() + + # Pay the invoices using a cash journal associated to the main company + ctx = {"active_model": "account.move", "active_ids": invoices.ids} + 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": self.payment_method_manual_in.id, + } + ) + + register_payments.action_create_payments() + payments = self.payment_model.search([], order="id desc", limit=2) + for payment in payments: + # Validate that inter OU balance move lines are created + self.assertEqual(len(payment.move_id.line_ids), 4) + self.assertAlmostEqual(payment.amount, 115000) + self.assertEqual(payment.state, "posted") + 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 = self.payment_model.create( + { + "payment_type": "outbound", + "amount": 115000, + "date": time.strftime("%Y") + "-07-15", + "journal_id": self.cash_journal_ou1.id, + "destination_journal_id": self.cash2_journal_b2b.id, + "destination_account_id": self.company.transfer_account_id.id, + "payment_method_line_id": self.payment_method_manual_in.id, + "is_internal_transfer": True, + } + ) + payment.action_post() + payments = payment + payment.paired_internal_transfer_payment_id + self.assertEqual(len(payments.move_id.mapped("line_ids.operating_unit_id")), 2) + # 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..9bfe8edbb8 --- /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..a57eb45e8d --- /dev/null +++ b/account_operating_unit/views/account_move_view.xml @@ -0,0 +1,160 @@ + + + + + + + account.move.line.form + account.move.line + + + + + + + + + account.move.line.tree + account.move.line + + + + + + + + + + Journal Items + account.move.line + + + + + + + + + + + + + + + account.move.form + account.move + + + + + + + + + + + + {'default_move_type': context.get('default_move_type'), + 'journal_id': + journal_id, 'default_partner_id': commercial_partner_id, + 'default_currency_id': currency_id or company_currency_id, + 'operating_unit_id': operating_unit_id} + + + + + { + 'default_move_type': context.get('default_move_type'), + 'line_ids': line_ids, + 'journal_id': journal_id, + 'default_partner_id': commercial_partner_id, + 'default_currency_id': currency_id or company_currency_id, + 'default_operating_unit_id': operating_unit_id} + + + + + + + + ['|', ('company_id', '=', parent.company_id), ('company_id', '=', + False), '|', '|', ('operating_unit_ids', '=', operating_unit_id), + ('operating_unit_ids', '=', False)] + + + + + + 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/account_operating_unit/wizards/__init__.py b/account_operating_unit/wizards/__init__.py new file mode 100644 index 0000000000..77e487c8c7 --- /dev/null +++ b/account_operating_unit/wizards/__init__.py @@ -0,0 +1,3 @@ +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from . import account_payment_register diff --git a/account_operating_unit/wizards/account_payment_register.py b/account_operating_unit/wizards/account_payment_register.py new file mode 100644 index 0000000000..0271b9fe95 --- /dev/null +++ b/account_operating_unit/wizards/account_payment_register.py @@ -0,0 +1,45 @@ +# © 2020 Jarsa Sistemas, SA de CV +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import _, models +from odoo.exceptions import UserError + + +class AccountPaymentRegister(models.TransientModel): + _inherit = "account.payment.register" + + def _create_payments(self): + payments = super()._create_payments() + if self.group_payment and len(payments) > 1: + return payments + for payment in payments: + to_reconcile = self.env["account.move.line"] + reconciled_moves = ( + payment.reconciled_bill_ids + payment.reconciled_invoice_ids + ) + if len(reconciled_moves.operating_unit_id) > 1: + raise UserError( + _( + "The OU in the Bills/Invoices to register payment must be the same." + ) + ) + if reconciled_moves.operating_unit_id != payment.operating_unit_id: + destination_account = payment.destination_account_id + line = payment.move_id.line_ids.filtered( + lambda l: l.account_id == destination_account + ) + line.write( + { + "operating_unit_id": reconciled_moves.operating_unit_id.id, + } + ) + # reconcile with case self balanced only + if payment.move_id.company_id.ou_is_self_balanced: + to_reconcile |= line + to_reconcile |= reconciled_moves.line_ids.filtered( + lambda l: l.account_id == destination_account + ) + payment.action_draft() + payment.action_post() + to_reconcile.filtered(lambda r: not r.reconciled).reconcile() + return payments diff --git a/setup/account_operating_unit/odoo/addons/account_operating_unit b/setup/account_operating_unit/odoo/addons/account_operating_unit new file mode 120000 index 0000000000..68b8276afc --- /dev/null +++ b/setup/account_operating_unit/odoo/addons/account_operating_unit @@ -0,0 +1 @@ +../../../../account_operating_unit \ No newline at end of file diff --git a/setup/account_operating_unit/setup.py b/setup/account_operating_unit/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/account_operating_unit/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)