Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions product_abc_classification/README.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

==========================
Product Abc Classification
==========================
Expand All @@ -17,7 +13,7 @@ Product Abc Classification
.. |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-AGPL--3-blue.png
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fproduct--attribute-lightgray.png?logo=github
Expand Down
3 changes: 2 additions & 1 deletion product_abc_classification/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"name": "Product Abc Classification",
"summary": """
ABC classification for sales and warehouse management""",
"version": "18.0.1.0.0",
"version": "18.0.1.1.0",
"license": "AGPL-3",
"author": "ACSONE SA/NV, ForgeFlow, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/product-attribute",
Expand All @@ -18,6 +18,7 @@
"views/product_product.xml",
"views/product_category.xml",
"security/ir.model.access.csv",
"security/security.xml",
"data/ir_cron.xml",
],
}
26 changes: 26 additions & 0 deletions product_abc_classification/migrations/18.0.1.1.0/post-migration.py
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -43,18 +44,27 @@ class AbcClassificationProductLevel(models.Model):
index=True,
required=True,
ondelete="cascade",
check_company=True,
)
product_tmpl_id = fields.Many2one(
"product.template",
string="Product template",
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",
Expand Down Expand Up @@ -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:
Expand Down
28 changes: 28 additions & 0 deletions product_abc_classification/models/abc_classification_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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:
Expand Down
1 change: 1 addition & 0 deletions product_abc_classification/models/product_product.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions product_abc_classification/models/product_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
19 changes: 19 additions & 0 deletions product_abc_classification/security/security.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="abc_classification_profile_rule" model="ir.rule">
<field name="name">ABC Classification Profile multi-company</field>
<field name="model_id" ref="model_abc_classification_profile" />
<field eval="True" name="global" />
<field
name="domain_force"
>['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="abc_classification_product_level_rule" model="ir.rule">
<field name="name">ABC Classification Product Level multi-company</field>
<field name="model_id" ref="model_abc_classification_product_level" />
<field eval="True" name="global" />
<field
name="domain_force"
>['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
</odoo>
28 changes: 11 additions & 17 deletions product_abc_classification/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>README.rst</title>
<title>Product Abc Classification</title>
<style type="text/css">

/*
Expand Down Expand Up @@ -360,21 +360,16 @@
</style>
</head>
<body>
<div class="document">
<div class="document" id="product-abc-classification">
<h1 class="title">Product Abc Classification</h1>


<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
</a>
<div class="section" id="product-abc-classification">
<h1>Product Abc Classification</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:f602d3cbe4b034e608e4a5b554b3532e87e682d94cb1ee8ba499905258e348c3
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/product-attribute/tree/18.0/product_abc_classification"><img alt="OCA/product-attribute" src="https://img.shields.io/badge/github-OCA%2Fproduct--attribute-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/product-attribute-18-0/product-attribute-18-0-product_abc_classification"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/product-attribute&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/product-attribute/tree/18.0/product_abc_classification"><img alt="OCA/product-attribute" src="https://img.shields.io/badge/github-OCA%2Fproduct--attribute-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/product-attribute-18-0/product-attribute-18-0-product_abc_classification"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/product-attribute&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This modules provides the bases to build ABC analysis (or ABC
classification) addons. These classification are used by inventory
management teams to help identify the most important products in their
Expand All @@ -400,7 +395,7 @@ <h1>Product Abc Classification</h1>
</ul>
</div>
<div class="section" id="usage">
<h2><a class="toc-backref" href="#toc-entry-1">Usage</a></h2>
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
<p>To use this module, you need to:</p>
<p>#. Go to Inventory menu, then to Configuration/Products/ABC
Classification Profile and create a profile with levels, knowing that
Expand All @@ -413,24 +408,24 @@ <h2><a class="toc-backref" href="#toc-entry-1">Usage</a></h2>
child categories and products will be profiled (or unprofiled).</p>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h2>
<h1><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/product-attribute/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/OCA/product-attribute/issues/new?body=module:%20product_abc_classification%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2><a class="toc-backref" href="#toc-entry-3">Credits</a></h2>
<h1><a class="toc-backref" href="#toc-entry-3">Credits</a></h1>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-4">Authors</a></h3>
<h2><a class="toc-backref" href="#toc-entry-4">Authors</a></h2>
<ul class="simple">
<li>ACSONE SA/NV</li>
<li>ForgeFlow</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-5">Contributors</a></h3>
<h2><a class="toc-backref" href="#toc-entry-5">Contributors</a></h2>
<ul class="simple">
<li>Miquel Raïch &lt;<a class="reference external" href="mailto:miquel.raich&#64;eficent.com">miquel.raich&#64;eficent.com</a>&gt;</li>
<li>Lindsay Marion &lt;<a class="reference external" href="mailto:lindsay.marion&#64;acsone.eu">lindsay.marion&#64;acsone.eu</a>&gt;</li>
Expand All @@ -440,12 +435,12 @@ <h3><a class="toc-backref" href="#toc-entry-5">Contributors</a></h3>
</ul>
</div>
<div class="section" id="other-credits">
<h3><a class="toc-backref" href="#toc-entry-6">Other credits</a></h3>
<h2><a class="toc-backref" href="#toc-entry-6">Other credits</a></h2>
<p>The migration of this module from 17.0 to 18.0 was financially supported
by Camptocamp</p>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h3>
<h2><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
Expand All @@ -458,6 +453,5 @@ <h3><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h3>
</div>
</div>
</div>
</div>
</body>
</html>
1 change: 1 addition & 0 deletions product_abc_classification/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from . import test_abc_classification_product_level
from . import test_abc_classification_profile
from . import test_product
from . import test_multi_company
83 changes: 83 additions & 0 deletions product_abc_classification/tests/test_multi_company.py
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
<group>
<group name="information" string="Profile Information">
<field name="profile_type" />
<field
name="company_id"
groups="base.group_multi_company"
/>
</group>
<group name="computation" string="Computation">
<field name="period" />
Expand Down Expand Up @@ -67,6 +71,7 @@
<field name="arch" type="xml">
<list>
<field name="name" />
<field name="company_id" groups="base.group_multi_company" />
</list>
</field>
</record>
Expand Down
Loading