diff --git a/README.md b/README.md
index 6f75bb27ce3..7cdfec7f28d 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ addon | version | maintainers | summary
[nutritional_info](nutritional_info/) | 18.0.1.0.0 |
| Nutritional information.
[nutritional_info_stock_lot](nutritional_info_stock_lot/) | 18.0.1.0.0 |
| Nutritional information for lots.
[pos_product_cost_security](pos_product_cost_security/) | 18.0.1.0.0 | | Compatibility between Point of Sale and Product Cost Security
-[product_abc_classification](product_abc_classification/) | 18.0.2.0.0 | | ABC classification for sales and warehouse management
+[product_abc_classification](product_abc_classification/) | 18.0.2.1.0 | | ABC classification for sales and warehouse management
[product_abc_classification_sale_stock](product_abc_classification_sale_stock/) | 18.0.1.0.0 |
| Compute ABC classification from the number of delivered sale order line by product
[product_assortment](product_assortment/) | 18.0.1.0.1 | | Adds the ability to manage products assortment
[product_attachment_zipped_download](product_attachment_zipped_download/) | 18.0.1.0.0 |
| Product Attachment Zipped Download
diff --git a/product_abc_classification/README.rst b/product_abc_classification/README.rst
index a315c8202fa..52459d95149 100644
--- a/product_abc_classification/README.rst
+++ b/product_abc_classification/README.rst
@@ -11,7 +11,7 @@ Product Abc Classification
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:7449408f8b05c5c6727fa26edf48fe6059b4c340378555629377ca12e0bb0b6b
+ !! source digest: sha256:0b89f1d5229ad6e783124e9478615b65f3122c6ca9c6a4da1445205147b44da2
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
diff --git a/product_abc_classification/__manifest__.py b/product_abc_classification/__manifest__.py
index fa4481d755d..dccee7bd4cf 100644
--- a/product_abc_classification/__manifest__.py
+++ b/product_abc_classification/__manifest__.py
@@ -6,7 +6,7 @@
"name": "Product Abc Classification",
"summary": """
ABC classification for sales and warehouse management""",
- "version": "18.0.2.0.0",
+ "version": "18.0.2.1.0",
"license": "AGPL-3",
"author": "ACSONE SA/NV, ForgeFlow, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/product-attribute",
@@ -18,6 +18,7 @@
"views/product_product.xml",
"views/product_category.xml",
"security/ir.model.access.csv",
+ "security/security.xml",
"data/ir_cron.xml",
],
}
diff --git a/product_abc_classification/i18n/es.po b/product_abc_classification/i18n/es.po
index 042c8a52ec8..322405f7389 100644
--- a/product_abc_classification/i18n/es.po
+++ b/product_abc_classification/i18n/es.po
@@ -140,6 +140,12 @@ msgstr "El nivel de clasificación es obligatorio"
msgid "Classification not in sync with computed"
msgstr "Clasificación no sincronizada con el cálculo"
+#. module: product_abc_classification
+#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__company_id
+#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__company_id
+msgid "Company"
+msgstr ""
+
#. module: product_abc_classification
#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_form_view
msgid "Computation"
@@ -398,6 +404,15 @@ msgstr "El nombre del perfil debe ser único"
msgid "Ratings"
msgstr ""
+#. module: product_abc_classification
+#. odoo-python
+#: code:addons/product_abc_classification/models/abc_classification_profile.py:0
+msgid ""
+"The ABC Classification Profile %(profile)s is assigned to company %"
+"(company)s, but the following products belong to another company: %"
+"(products)s."
+msgstr ""
+
#. module: product_abc_classification
#. odoo-python
#: code:addons/product_abc_classification/models/abc_classification_level.py:0
diff --git a/product_abc_classification/i18n/fr.po b/product_abc_classification/i18n/fr.po
index df3147a1df2..1da6dcead6a 100644
--- a/product_abc_classification/i18n/fr.po
+++ b/product_abc_classification/i18n/fr.po
@@ -140,6 +140,12 @@ msgstr "La classe / niveau est obligatoire"
msgid "Classification not in sync with computed"
msgstr "Classes ABC manuelle et calculée divergentes"
+#. module: product_abc_classification
+#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__company_id
+#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__company_id
+msgid "Company"
+msgstr ""
+
#. module: product_abc_classification
#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_form_view
msgid "Computation"
@@ -404,6 +410,15 @@ msgstr "Le nom du profil doit être unique"
msgid "Ratings"
msgstr ""
+#. module: product_abc_classification
+#. odoo-python
+#: code:addons/product_abc_classification/models/abc_classification_profile.py:0
+msgid ""
+"The ABC Classification Profile %(profile)s is assigned to company %"
+"(company)s, but the following products belong to another company: %"
+"(products)s."
+msgstr ""
+
#. module: product_abc_classification
#. odoo-python
#: code:addons/product_abc_classification/models/abc_classification_level.py:0
diff --git a/product_abc_classification/i18n/it.po b/product_abc_classification/i18n/it.po
index 29a37782884..9f927382985 100644
--- a/product_abc_classification/i18n/it.po
+++ b/product_abc_classification/i18n/it.po
@@ -140,6 +140,12 @@ msgstr "Il livello classificazione è obbligatorio"
msgid "Classification not in sync with computed"
msgstr "Classificazione non sincronizzata con il calcolo"
+#. module: product_abc_classification
+#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__company_id
+#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__company_id
+msgid "Company"
+msgstr ""
+
#. module: product_abc_classification
#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_form_view
msgid "Computation"
@@ -398,6 +404,15 @@ msgstr "Il nome profilo deve essere univoco"
msgid "Ratings"
msgstr ""
+#. module: product_abc_classification
+#. odoo-python
+#: code:addons/product_abc_classification/models/abc_classification_profile.py:0
+msgid ""
+"The ABC Classification Profile %(profile)s is assigned to company %"
+"(company)s, but the following products belong to another company: %"
+"(products)s."
+msgstr ""
+
#. module: product_abc_classification
#. odoo-python
#: code:addons/product_abc_classification/models/abc_classification_level.py:0
diff --git a/product_abc_classification/i18n/product_abc_classification.pot b/product_abc_classification/i18n/product_abc_classification.pot
index e08bff691b1..0a7f1f6cacd 100644
--- a/product_abc_classification/i18n/product_abc_classification.pot
+++ b/product_abc_classification/i18n/product_abc_classification.pot
@@ -135,6 +135,12 @@ msgstr ""
msgid "Classification not in sync with computed"
msgstr ""
+#. module: product_abc_classification
+#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__company_id
+#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__company_id
+msgid "Company"
+msgstr ""
+
#. module: product_abc_classification
#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_form_view
msgid "Computation"
@@ -387,6 +393,15 @@ msgstr ""
msgid "Ratings"
msgstr ""
+#. module: product_abc_classification
+#. odoo-python
+#: code:addons/product_abc_classification/models/abc_classification_profile.py:0
+msgid ""
+"The ABC Classification Profile %(profile)s is assigned to company "
+"%(company)s, but the following products belong to another company: "
+"%(products)s."
+msgstr ""
+
#. module: product_abc_classification
#. odoo-python
#: code:addons/product_abc_classification/models/abc_classification_level.py:0
diff --git a/product_abc_classification/migrations/18.0.2.1.0/post-migration.py b/product_abc_classification/migrations/18.0.2.1.0/post-migration.py
new file mode 100644
index 00000000000..df84acdd11a
--- /dev/null
+++ b/product_abc_classification/migrations/18.0.2.1.0/post-migration.py
@@ -0,0 +1,26 @@
+# Copyright 2026 ForgeFlow
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+import logging
+
+_logger = logging.getLogger(__name__)
+
+
+def migrate(cr, version):
+ if not version:
+ return
+ cr.execute(
+ """
+ UPDATE abc_classification_product_level AS lvl
+ SET company_id = tmpl.company_id
+ FROM product_product AS pp,
+ product_template AS tmpl
+ WHERE lvl.product_id = pp.id
+ AND pp.product_tmpl_id = tmpl.id
+ AND tmpl.company_id IS NOT NULL
+ AND lvl.company_id IS NULL;
+ """
+ )
+ _logger.info(
+ "Backfilled company_id on %s abc.classification.product.level rows",
+ cr.rowcount,
+ )
diff --git a/product_abc_classification/models/abc_classification_product_level.py b/product_abc_classification/models/abc_classification_product_level.py
index 505642afb2d..bd6d13b38f0 100644
--- a/product_abc_classification/models/abc_classification_product_level.py
+++ b/product_abc_classification/models/abc_classification_product_level.py
@@ -10,6 +10,7 @@ class AbcClassificationProductLevel(models.Model):
_inherit = "mail.thread"
_description = "Abc Classification Product Level"
_rec_name = "level_id"
+ _check_company_auto = True
manual_level_id = fields.Many2one(
"abc.classification.level",
@@ -43,6 +44,7 @@ class AbcClassificationProductLevel(models.Model):
index=True,
required=True,
ondelete="cascade",
+ check_company=True,
)
product_tmpl_id = fields.Many2one(
"product.template",
@@ -50,11 +52,19 @@ class AbcClassificationProductLevel(models.Model):
index=True,
readonly=True,
)
+ company_id = fields.Many2one(
+ "res.company",
+ compute="_compute_company_id",
+ store=True,
+ readonly=True,
+ index=True,
+ )
# percentage
profile_id = fields.Many2one(
"abc.classification.profile",
string="Profile",
required=True,
+ check_company=True,
)
profile_type = fields.Selection(
related="profile_id.profile_type",
@@ -124,6 +134,13 @@ def _compute_flag(self):
rec.computed_level_id and rec.manual_level_id != rec.computed_level_id
)
+ @api.depends("product_id.company_id", "profile_id.company_id")
+ def _compute_company_id(self):
+ for rec in self:
+ rec.company_id = (
+ rec.profile_id.company_id or rec.product_id.company_id or False
+ )
+
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
diff --git a/product_abc_classification/models/abc_classification_profile.py b/product_abc_classification/models/abc_classification_profile.py
index 7a7ef7da7a3..d1c2c174b5d 100644
--- a/product_abc_classification/models/abc_classification_profile.py
+++ b/product_abc_classification/models/abc_classification_profile.py
@@ -28,6 +28,10 @@ class AbcClassificationProfile(models.Model):
string="Period on which to compute the classification (Days)",
required=True,
)
+ company_id = fields.Many2one(
+ comodel_name="res.company",
+ string="Company",
+ )
product_variant_ids = fields.Many2many(
comodel_name="product.product",
@@ -47,6 +51,30 @@ class AbcClassificationProfile(models.Model):
_sql_constraints = [("name_uniq", "UNIQUE(name)", "Profile name must be unique")]
+ @api.constrains("company_id", "product_variant_ids")
+ def _check_company_products(self):
+ for profile in self:
+ if not profile.company_id:
+ continue
+ bad = self.env["product.product"].search(
+ [
+ ("id", "in", profile.product_variant_ids.ids),
+ ("company_id", "!=", False),
+ ("company_id", "!=", profile.company_id.id),
+ ]
+ )
+ if bad:
+ raise ValidationError(
+ self.env._(
+ "The ABC Classification Profile %(profile)s is assigned "
+ "to company %(company)s, but the following products "
+ "belong to another company: %(products)s.",
+ profile=profile.display_name,
+ company=profile.company_id.display_name,
+ products=", ".join(bad.mapped("display_name")),
+ )
+ )
+
@api.constrains("level_ids")
def _check_levels(self):
for profile in self:
diff --git a/product_abc_classification/models/product_product.py b/product_abc_classification/models/product_product.py
index 31c79263d73..07196ef15ee 100644
--- a/product_abc_classification/models/product_product.py
+++ b/product_abc_classification/models/product_product.py
@@ -17,6 +17,7 @@ class ProductProduct(models.Model):
column1="product_id",
column2="profile_id",
index=True,
+ check_company=True,
)
abc_classification_profile_updatable_from_category = fields.Boolean(default=True)
diff --git a/product_abc_classification/models/product_template.py b/product_abc_classification/models/product_template.py
index 48c01bbc47d..2f73ca74e40 100644
--- a/product_abc_classification/models/product_template.py
+++ b/product_abc_classification/models/product_template.py
@@ -13,6 +13,7 @@ class ProductTemplate(models.Model):
compute="_compute_abc_classification_profile_ids",
inverse="_inverse_abc_classification_profile_ids",
store=True,
+ check_company=True,
)
abc_classification_product_level_ids = fields.One2many(
"abc.classification.product.level",
diff --git a/product_abc_classification/security/security.xml b/product_abc_classification/security/security.xml
new file mode 100644
index 00000000000..9663d5b74df
--- /dev/null
+++ b/product_abc_classification/security/security.xml
@@ -0,0 +1,19 @@
+
+
This modules provides the bases to build ABC analysis (or ABC
diff --git a/product_abc_classification/tests/__init__.py b/product_abc_classification/tests/__init__.py
index 644a77664ca..f1fc3141b68 100644
--- a/product_abc_classification/tests/__init__.py
+++ b/product_abc_classification/tests/__init__.py
@@ -2,3 +2,4 @@
from . import test_abc_classification_profile
from . import test_abc_classification_manual_profile
from . import test_product
+from . import test_multi_company
diff --git a/product_abc_classification/tests/test_multi_company.py b/product_abc_classification/tests/test_multi_company.py
new file mode 100644
index 00000000000..8750d10f764
--- /dev/null
+++ b/product_abc_classification/tests/test_multi_company.py
@@ -0,0 +1,83 @@
+# Copyright 2026 ForgeFlow
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo.exceptions import UserError, ValidationError
+from odoo.tests.common import tagged
+
+from .common import ABCClassificationLevelCase
+
+
+@tagged("post_install", "-at_install")
+class TestMultiCompany(ABCClassificationLevelCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.company_a = cls.env["res.company"].create({"name": "Company A"})
+ cls.company_b = cls.env["res.company"].create({"name": "Company B"})
+ cls.product_in_a = cls.env["product.product"].create(
+ {"name": "Prod A", "company_id": cls.company_a.id}
+ )
+ cls.product_in_b = cls.env["product.product"].create(
+ {"name": "Prod B", "company_id": cls.company_b.id}
+ )
+ cls.product_shared = cls.env["product.product"].create({"name": "Prod Shared"})
+
+ def test_shared_profile_inherits_product_company(self):
+ self.classification_profile.company_id = False
+ self.product_in_a.abc_classification_profile_ids = self.classification_profile
+ self.classification_profile._compute_abc_classification()
+ level = self.ProductLevel.search(
+ [
+ ("profile_id", "=", self.classification_profile.id),
+ ("product_id", "=", self.product_in_a.id),
+ ]
+ )
+ self.assertEqual(level.company_id, self.company_a)
+
+ def test_company_profile_overrides_product_company(self):
+ self.classification_profile.company_id = self.company_a
+ self.product_shared.abc_classification_profile_ids = self.classification_profile
+ self.classification_profile._compute_abc_classification()
+ level = self.ProductLevel.search(
+ [
+ ("profile_id", "=", self.classification_profile.id),
+ ("product_id", "=", self.product_shared.id),
+ ]
+ )
+ self.assertEqual(level.company_id, self.company_a)
+
+ def test_profile_constraint_conflicting_products(self):
+ self.product_in_b.abc_classification_profile_ids = self.classification_profile
+ with self.assertRaises(ValidationError):
+ self.classification_profile.company_id = self.company_a
+
+ def test_level_blocks_conflicting_profile_and_product(self):
+ self.classification_profile.company_id = self.company_a
+ with self.assertRaises(UserError):
+ self.ProductLevel.create(
+ {
+ "profile_id": self.classification_profile.id,
+ "product_id": self.product_in_b.id,
+ "manual_level_id": self.classification_level_a.id,
+ }
+ )
+
+ def test_cannot_change_product_company_with_conflicting_profile(self):
+ self.classification_profile.company_id = self.company_a
+ self.product_shared.abc_classification_profile_ids = self.classification_profile
+ with self.assertRaises(UserError):
+ self.product_shared.company_id = self.company_b
+
+ def test_level_company_recomputes_on_profile_change(self):
+ self.classification_profile.company_id = False
+ self.product_shared.abc_classification_profile_ids = self.classification_profile
+ self.classification_profile._compute_abc_classification()
+ level = self.ProductLevel.search(
+ [
+ ("profile_id", "=", self.classification_profile.id),
+ ("product_id", "=", self.product_shared.id),
+ ]
+ )
+ self.assertFalse(level.company_id)
+ self.classification_profile.company_id = self.company_a
+ self.assertEqual(level.company_id, self.company_a)
diff --git a/product_abc_classification/views/abc_classification_profile.xml b/product_abc_classification/views/abc_classification_profile.xml
index 5af737940c6..69e9607fc03 100644
--- a/product_abc_classification/views/abc_classification_profile.xml
+++ b/product_abc_classification/views/abc_classification_profile.xml
@@ -32,6 +32,10 @@