diff --git a/crm_lead_company_currency_fix/README.rst b/crm_lead_company_currency_fix/README.rst new file mode 100644 index 00000000000..0a132ae6d90 --- /dev/null +++ b/crm_lead_company_currency_fix/README.rst @@ -0,0 +1,135 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +======================= +CRM - Fix lead currency +======================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:34af6785c2aaa8fa8b396b8fae36790aa96db6030caf8ac27affbdb1f14f3088 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fcrm-lightgray.png?logo=github + :target: https://github.com/OCA/crm/tree/19.0/crm_lead_company_currency_fix + :alt: OCA/crm +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/crm-19-0/crm-19-0-crm_lead_company_currency_fix + :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/crm&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +In Odoo standard source code, leads' currency is defined as a computed, +non-stored field, which follows this workflow: + +- if the lead's company is set, then the company currency is used + +- if the lead's company is not set, then the user's company currency is + used + +Since the field is not stored, it leads to 2 main issues: + +1) Changing a company's currency will change the currency on all the + existing leads linked to that company, but not the amounts. Eg: + + - you have a lead linked to a company in EUR + - the lead's expected revenue is 1000 EUR + - you change the company currency to USD + - the lead's expected revenue becomes 1000 USD + +2) If a lead is not linked to a specific company, then 2 users that are + logged in with 2 different companies and different currencies will + see the lead's amounts with different currencies. Eg: + + - you have a lead where the company is not set + - accessing the lead with a user whose main company is in EUR will + display an expected revenue of 1000 EUR + - accessing the lead with a user whose main company is in USD will + display an expected revenue of 1000 USD + +This module stores the field in the DB to keep data consistency, and +will only update the lead's currency only if the lead's company itself +is updated. The behavior for computing the lead's currency will remain +the same (currency is retrieved from the lead's company or the current +user's company), but the issues are fixed: + +1) Changing a company's currency **will not change the currency on + existing leads**, only on the ones created after the currency has + been updated. Eg: + + - you have a lead linked to a company in EUR + - the lead's expected revenue is 1000 EUR + - you change the company currency to USD + - the lead's expected revenue is still 1000 EUR + - a newly created lead's expected revenue will be in USD, not EUR + +2) If a lead is not linked to a specific company, then 2 users that are + logged in with 2 different companies and different currencies will + see the **lead's amounts with the same currency** (computed from the + company of the first user that triggers the recomputation). Eg: + + - you have a lead where the company is not set + - accessing the lead with a 1st user whose main company is in EUR + will display an expected revenue of 1000 EUR + - accessing the lead with a 2nd user whose main company is in USD + will display an expected revenue of 1000 EUR, because the currency + was set from the previous user's company + +**Table of contents** + +.. contents:: + :local: + +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 +------- + +* Camptocamp + +Contributors +------------ + +- Silvio Gregorini (`Camptocamp + SA `__) + +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/crm `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/crm_lead_company_currency_fix/__init__.py b/crm_lead_company_currency_fix/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/crm_lead_company_currency_fix/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/crm_lead_company_currency_fix/__manifest__.py b/crm_lead_company_currency_fix/__manifest__.py new file mode 100644 index 00000000000..206ed4d6b14 --- /dev/null +++ b/crm_lead_company_currency_fix/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2026 Camptocamp SA +# License LGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "CRM - Fix lead currency", + "summary": "Fixes usage of leads' currencies", + "version": "19.0.1.0.0", + "category": "Sales/CRM", + "website": "https://github.com/OCA/crm", + "author": "Camptocamp, Odoo Community Association (OCA)", + "license": "LGPL-3", + "depends": [ + # Odoo + "crm", + ], + # Make sure this module is installed as soon as possible + # after ``crm`` is installed: this way, the override of + # ``crm.lead._field_to_sql()`` will work correctly + "sequence": 0, +} diff --git a/crm_lead_company_currency_fix/models/__init__.py b/crm_lead_company_currency_fix/models/__init__.py new file mode 100644 index 00000000000..e66f0d6cf4e --- /dev/null +++ b/crm_lead_company_currency_fix/models/__init__.py @@ -0,0 +1 @@ +from . import crm_lead diff --git a/crm_lead_company_currency_fix/models/crm_lead.py b/crm_lead_company_currency_fix/models/crm_lead.py new file mode 100644 index 00000000000..886fbf33d39 --- /dev/null +++ b/crm_lead_company_currency_fix/models/crm_lead.py @@ -0,0 +1,20 @@ +# Copyright 2026 Camptocamp SA +# License LGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class CRMLead(models.Model): + _inherit = "crm.lead" + + # OVERRIDE: make ``company_currency`` a stored field + company_currency = fields.Many2one(store=True) + + def _field_to_sql(self, alias, field_expr, query=None): + # OVERRIDE: module ``crm`` override for ``field_expr == "company_currency"`` + # creates a SQL object that represents the dynamic nature of the original + # computed, non-stored field. We need to ignore that to use the DB-stored + # values instead. + if field_expr == "company_currency": + return models.Model._field_to_sql(self, alias, field_expr, query=query) + return super()._field_to_sql(alias, field_expr, query=query) diff --git a/crm_lead_company_currency_fix/pyproject.toml b/crm_lead_company_currency_fix/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/crm_lead_company_currency_fix/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/crm_lead_company_currency_fix/readme/CONTRIBUTORS.md b/crm_lead_company_currency_fix/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..6e790bc99ca --- /dev/null +++ b/crm_lead_company_currency_fix/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Silvio Gregorini \<\> ([Camptocamp SA](https://www.camptocamp.com/)) diff --git a/crm_lead_company_currency_fix/readme/DESCRIPTION.md b/crm_lead_company_currency_fix/readme/DESCRIPTION.md new file mode 100644 index 00000000000..f00c34efc73 --- /dev/null +++ b/crm_lead_company_currency_fix/readme/DESCRIPTION.md @@ -0,0 +1,48 @@ +In Odoo standard source code, leads' currency is defined as a computed, non-stored +field, which follows this workflow: + +- if the lead's company is set, then the company currency is used + +- if the lead's company is not set, then the user's company currency is used + +Since the field is not stored, it leads to 2 main issues: + +1) Changing a company's currency will change the currency on all the existing leads + linked to that company, but not the amounts. Eg: + - you have a lead linked to a company in EUR + - the lead's expected revenue is 1000 EUR + - you change the company currency to USD + - the lead's expected revenue becomes 1000 USD + +2) If a lead is not linked to a specific company, then 2 users that are logged in with + 2 different companies and different currencies will see the lead's amounts with + different currencies. Eg: + - you have a lead where the company is not set + - accessing the lead with a user whose main company is in EUR will display an + expected revenue of 1000 EUR + - accessing the lead with a user whose main company is in USD will display an + expected revenue of 1000 USD + +This module stores the field in the DB to keep data consistency, and will only update +the lead's currency only if the lead's company itself is updated. The behavior for +computing the lead's currency will remain the same (currency is retrieved from the +lead's company or the current user's company), but the issues are fixed: + +1) Changing a company's currency **will not change the currency on existing leads**, + only on the ones created after the currency has been updated. Eg: + - you have a lead linked to a company in EUR + - the lead's expected revenue is 1000 EUR + - you change the company currency to USD + - the lead's expected revenue is still 1000 EUR + - a newly created lead's expected revenue will be in USD, not EUR + +2) If a lead is not linked to a specific company, then 2 users that are logged in with + 2 different companies and different currencies will see the **lead's amounts with + the same currency** (computed from the company of the first user that triggers the + recomputation). Eg: + - you have a lead where the company is not set + - accessing the lead with a 1st user whose main company is in EUR will display an + expected revenue of 1000 EUR + - accessing the lead with a 2nd user whose main company is in USD will display an + expected revenue of 1000 EUR, because the currency was set from the previous + user's company diff --git a/crm_lead_company_currency_fix/static/description/index.html b/crm_lead_company_currency_fix/static/description/index.html new file mode 100644 index 00000000000..11fa97498ac --- /dev/null +++ b/crm_lead_company_currency_fix/static/description/index.html @@ -0,0 +1,486 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

CRM - Fix lead currency

+ +

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

+

In Odoo standard source code, leads’ currency is defined as a computed, +non-stored field, which follows this workflow:

+
    +
  • if the lead’s company is set, then the company currency is used
  • +
  • if the lead’s company is not set, then the user’s company currency is +used
  • +
+

Since the field is not stored, it leads to 2 main issues:

+
    +
  1. Changing a company’s currency will change the currency on all the +existing leads linked to that company, but not the amounts. Eg:
      +
    • you have a lead linked to a company in EUR
    • +
    • the lead’s expected revenue is 1000 EUR
    • +
    • you change the company currency to USD
    • +
    • the lead’s expected revenue becomes 1000 USD
    • +
    +
  2. +
  3. If a lead is not linked to a specific company, then 2 users that are +logged in with 2 different companies and different currencies will +see the lead’s amounts with different currencies. Eg:
      +
    • you have a lead where the company is not set
    • +
    • accessing the lead with a user whose main company is in EUR will +display an expected revenue of 1000 EUR
    • +
    • accessing the lead with a user whose main company is in USD will +display an expected revenue of 1000 USD
    • +
    +
  4. +
+

This module stores the field in the DB to keep data consistency, and +will only update the lead’s currency only if the lead’s company itself +is updated. The behavior for computing the lead’s currency will remain +the same (currency is retrieved from the lead’s company or the current +user’s company), but the issues are fixed:

+
    +
  1. Changing a company’s currency will not change the currency on +existing leads, only on the ones created after the currency has +been updated. Eg:
      +
    • you have a lead linked to a company in EUR
    • +
    • the lead’s expected revenue is 1000 EUR
    • +
    • you change the company currency to USD
    • +
    • the lead’s expected revenue is still 1000 EUR
    • +
    • a newly created lead’s expected revenue will be in USD, not EUR
    • +
    +
  2. +
  3. If a lead is not linked to a specific company, then 2 users that are +logged in with 2 different companies and different currencies will +see the lead’s amounts with the same currency (computed from the +company of the first user that triggers the recomputation). Eg:
      +
    • you have a lead where the company is not set
    • +
    • accessing the lead with a 1st user whose main company is in EUR +will display an expected revenue of 1000 EUR
    • +
    • accessing the lead with a 2nd user whose main company is in USD +will display an expected revenue of 1000 EUR, because the currency +was set from the previous user’s company
    • +
    +
  4. +
+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+ +
+

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/crm project on GitHub.

+

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

+
+
+
+
+ + diff --git a/crm_lead_company_currency_fix/tests/__init__.py b/crm_lead_company_currency_fix/tests/__init__.py new file mode 100644 index 00000000000..f4a7f0d37b0 --- /dev/null +++ b/crm_lead_company_currency_fix/tests/__init__.py @@ -0,0 +1 @@ +from . import test_crm_lead_company_currency_fix diff --git a/crm_lead_company_currency_fix/tests/common.py b/crm_lead_company_currency_fix/tests/common.py new file mode 100644 index 00000000000..11aa25e9ffd --- /dev/null +++ b/crm_lead_company_currency_fix/tests/common.py @@ -0,0 +1,49 @@ +# Copyright 2026 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +from odoo.orm.commands import Command +from odoo.tests.common import new_test_user + +from odoo.addons.base.tests.common import BaseCommon + + +class Common(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Setup EUR currency and company + cls.currency_eur = cls.env.ref("base.EUR") + cls.company_eur = cls.env["res.company"].create({"name": "Company EUR"}) + cls.company_eur.currency_id = cls.currency_eur + # Setup CHF currency and company + cls.currency_chf = cls.env.ref("base.CHF") + cls.company_chf = cls.env["res.company"].create({"name": "Company CHF"}) + cls.company_chf.currency_id = cls.currency_chf + # Setup test users, making sure they have access to both companies but are + # logged in to different companies by default + cls.user_eur = new_test_user( + cls.env, + login="user_eur", + groups="sales_team.group_sale_salesman", + **dict( + name="User EUR", + email="user-eur@test.com", + company_id=cls.company_eur.id, + company_ids=[Command.set((cls.company_eur + cls.company_chf).ids)], + ), + ) + cls.user_chf = new_test_user( + cls.env, + login="user_chf", + groups="sales_team.group_sale_salesman", + **dict( + name="User chf", + email="user-chf@test.com", + company_id=cls.company_chf.id, + company_ids=[Command.set((cls.company_eur + cls.company_chf).ids)], + ), + ) + + def _test_lead_company_currency(self, lead, expected_currency, user=None): + lead = lead.with_user(user or self.env.user) + self.assertEqual(lead.company_currency, expected_currency) diff --git a/crm_lead_company_currency_fix/tests/test_crm_lead_company_currency_fix.py b/crm_lead_company_currency_fix/tests/test_crm_lead_company_currency_fix.py new file mode 100644 index 00000000000..0506473b9f4 --- /dev/null +++ b/crm_lead_company_currency_fix/tests/test_crm_lead_company_currency_fix.py @@ -0,0 +1,45 @@ +# Copyright 2026 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) + +from odoo.tests.common import users + +from .common import Common + + +class TestCRMLeadCompanyCurrencyFix(Common): + @users("user_eur") + def test_00_lead_currency_preserved_after_company_currency_changes(self): + # Prepare the same values to use for 2 leads, one created before the currency + # update on the company, one after that + vals = {"name": "Lead", "company_id": self.company_eur.id} + + # Make sure that if you change the currency on the lead's company after the lead + # has already been created won't update the currency on the lead itself + lead_1 = self.env["crm.lead"].create(vals) + self._test_lead_company_currency(lead_1, self.currency_eur) + self.company_eur.currency_id = self.currency_chf + self._test_lead_company_currency(lead_1, self.currency_eur) + + # Check that a newly created lead will use the new currency instead + lead_2 = self.env["crm.lead"].create(vals) + self._test_lead_company_currency(lead_2, self.currency_chf) + + def test_01_currency_consistency_for_lead_without_company(self): + # Let the EUR user create a lead with no company + lead = ( + self.env["crm.lead"] + .with_user(self.user_eur) + .create( + { + "name": "Lead", + "company_id": False, + # Keep ``user_id`` empty to prevent ``AccessError`` when + # trying to read the lead's currency with the CHF user + "user_id": False, + } + ) + ) + + # Check the company is EUR for both EUR user and CHF user + self._test_lead_company_currency(lead, self.currency_eur, self.user_eur) + self._test_lead_company_currency(lead, self.currency_eur, self.user_chf)