From 203295893b0b226a4671e29d3d4878d62b5ab800 Mon Sep 17 00:00:00 2001 From: mreficent Date: Thu, 28 May 2020 13:35:47 +0200 Subject: [PATCH 01/22] [ADD] product_abc_classification --- product_abc_classification/__init__.py | 1 + product_abc_classification/__manifest__.py | 20 +++ product_abc_classification/data/ir_cron.xml | 13 ++ product_abc_classification/models/__init__.py | 4 + .../models/abc_classification_level.py | 35 ++++ .../models/abc_classification_profile.py | 156 ++++++++++++++++++ .../models/product_category.py | 26 +++ .../models/product_product.py | 74 +++++++++ .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 8 + product_abc_classification/readme/USAGE.rst | 11 ++ .../security/ir.model.access.csv | 5 + .../static/description/icon.png | Bin 0 -> 9455 bytes .../views/abc_classification_view.xml | 69 ++++++++ .../views/product_view.xml | 94 +++++++++++ 15 files changed, 517 insertions(+) create mode 100644 product_abc_classification/__init__.py create mode 100644 product_abc_classification/__manifest__.py create mode 100644 product_abc_classification/data/ir_cron.xml create mode 100644 product_abc_classification/models/__init__.py create mode 100644 product_abc_classification/models/abc_classification_level.py create mode 100644 product_abc_classification/models/abc_classification_profile.py create mode 100644 product_abc_classification/models/product_category.py create mode 100644 product_abc_classification/models/product_product.py create mode 100644 product_abc_classification/readme/CONTRIBUTORS.rst create mode 100644 product_abc_classification/readme/DESCRIPTION.rst create mode 100644 product_abc_classification/readme/USAGE.rst create mode 100644 product_abc_classification/security/ir.model.access.csv create mode 100644 product_abc_classification/static/description/icon.png create mode 100644 product_abc_classification/views/abc_classification_view.xml create mode 100644 product_abc_classification/views/product_view.xml diff --git a/product_abc_classification/__init__.py b/product_abc_classification/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/product_abc_classification/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/product_abc_classification/__manifest__.py b/product_abc_classification/__manifest__.py new file mode 100644 index 00000000000..d67fd6bc574 --- /dev/null +++ b/product_abc_classification/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2020 ForgeFlow +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Product ABC Classification", + "summary": "Includes ABC classification for inventory management", + "version": "13.0.1.0.0", + "author": "ForgeFlow, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/product-attribute", + "category": "Inventory Management", + "license": "AGPL-3", + "maintainers": ["MiquelRForgeFlow"], + "depends": ["sale_stock"], + "data": [ + "security/ir.model.access.csv", + "views/product_view.xml", + "views/abc_classification_view.xml", + "data/ir_cron.xml", + ], + "installable": True, +} diff --git a/product_abc_classification/data/ir_cron.xml b/product_abc_classification/data/ir_cron.xml new file mode 100644 index 00000000000..36faa7971a5 --- /dev/null +++ b/product_abc_classification/data/ir_cron.xml @@ -0,0 +1,13 @@ + + + + Perform the product ABC Classification + 1 + days + -1 + + + model._compute_abc_classification() + code + + diff --git a/product_abc_classification/models/__init__.py b/product_abc_classification/models/__init__.py new file mode 100644 index 00000000000..39394627c87 --- /dev/null +++ b/product_abc_classification/models/__init__.py @@ -0,0 +1,4 @@ +from . import product_category +from . import product_product +from . import abc_classification_level +from . import abc_classification_profile diff --git a/product_abc_classification/models/abc_classification_level.py b/product_abc_classification/models/abc_classification_level.py new file mode 100644 index 00000000000..2b6790c6b5b --- /dev/null +++ b/product_abc_classification/models/abc_classification_level.py @@ -0,0 +1,35 @@ +# Copyright 2020 ForgeFlow +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class ABCClasificationProfileLevel(models.Model): + _name = "abc.classification.profile.level" + _description = "ABC Clasification Profile Level" + _order = "percentage desc, id desc" + + percentage = fields.Float(default=0.0, required=True, string="%") + profile_id = fields.Many2one("abc.classification.profile") + + def name_get(self): + def _get_sort_key_percentage(rec): + return rec.percentage + + res = [] + for profile in self.mapped("profile_id"): + for i, level in enumerate( + profile.level_ids.sorted(key=_get_sort_key_percentage, reverse=True) + ): + name = "{} ({}%)".format(chr(65 + i), level.percentage) + res += [(level.id, name)] + return res + + @api.constrains("percentage") + def _check_percentage(self): + for level in self: + if level.percentage > 100.0: + raise ValidationError(_("The percentage cannot be greater than 100.")) + elif level.percentage <= 0.0: + raise ValidationError(_("The percentage should be a positive number.")) diff --git a/product_abc_classification/models/abc_classification_profile.py b/product_abc_classification/models/abc_classification_profile.py new file mode 100644 index 00000000000..f75be657558 --- /dev/null +++ b/product_abc_classification/models/abc_classification_profile.py @@ -0,0 +1,156 @@ +# Copyright 2020 ForgeFlow +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from datetime import timedelta + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class ABCClasificationProfile(models.Model): + _name = "abc.classification.profile" + _description = "ABC Clasification Profile" + + name = fields.Char() + level_ids = fields.One2many( + comodel_name="abc.classification.profile.level", inverse_name="profile_id" + ) + representation = fields.Char(compute="_compute_representation") + data_source = fields.Selection( + selection=[("stock_moves", "Stock Moves")], + default="stock_moves", + string="Data Source", + index=True, + required=True, + ) + value_criteria = fields.Selection( + selection=[("consumption_value", "Consumption Value")], + # others: 'sales revenue', 'profitability', ... + default="consumption_value", + string="Value", + index=True, + required=True, + ) + past_period = fields.Integer( + default=365, string="Past demand period (Days)", required=True + ) + + @api.depends("level_ids") + def _compute_representation(self): + def _get_sort_key_percentage(rec): + return rec.percentage + + for profile in self: + profile.level_ids.sorted(key=_get_sort_key_percentage, reverse=True) + profile.representation = "/".join( + [str(x) for x in profile.level_ids.mapped("display_name")] + ) + + @api.constrains("level_ids") + def _check_levels(self): + for profile in self: + percentages = profile.level_ids.mapped("percentage") + total = sum(percentages) + if profile.level_ids and total != 100.0: + raise ValidationError( + _("The sum of the percentages of the levels should be 100.") + ) + if profile.level_ids and len({}.fromkeys(percentages)) != len(percentages): + raise ValidationError( + _("The percentages of the levels must be unique.") + ) + + def write(self, vals): + return super().write(vals) + + def _fill_initial_product_data(self, date): + product_list = [] + if self.data_source == "stock_moves": + return self._fill_data_from_stock_moves(date, product_list) + else: + return product_list + + def _fill_data_from_stock_moves(self, date, product_list): + self.ensure_one() + moves = ( + self.env["stock.move"] + .sudo() + .read_group( + [ + ("state", "=", "done"), + ("date", ">", date), + ("location_dest_id.usage", "=", "customer"), + ("location_id.usage", "!=", "customer"), + ("product_id.type", "=", "product"), + "|", + ("product_id.abc_classification_profile_id", "=", self.id), + "|", + ("product_id.categ_id.abc_classification_profile_id", "=", self.id), + ( + "product_id.categ_id.parent_id.abc_classification_profile_id", + "=", + self.id, + ), + ], + ["product_id", "product_qty"], + ["product_id"], + ) + ) + for move in moves: + product_data = { + "product": self.env["product.product"].browse(move["product_id"][0]), + "units_sold": move["product_qty"], + } + product_list.append(product_data) + return product_list + + def _get_inventory_product_value(self, data): + self.ensure_one() + if self.value_criteria == "consumption_value": + return data["unit_cost"] * data["units_sold"] + raise 0.0 + + @api.model + def _compute_abc_classification(self): + def _get_sort_key_value(data): + return data["value"] + + def _get_sort_key_percentage(rec): + return rec.percentage + + profiles = self.search([]).filtered(lambda p: p.level_ids) + for profile in profiles: + oldest_date = fields.Datetime.to_string( + fields.Datetime.today() - timedelta(days=profile.past_period) + ) + totals = { + "units_sold": 0, + "value": 0.0, + } + product_list = profile._fill_initial_product_data(oldest_date) + for product_data in product_list: + product_data["unit_cost"] = product_data["product"].standard_price + totals["units_sold"] += product_data["units_sold"] + product_data["value"] = profile._get_inventory_product_value( + product_data + ) + totals["value"] += product_data["value"] + product_list.sort(reverse=True, key=_get_sort_key_value) + levels = profile.level_ids.sorted( + key=_get_sort_key_percentage, reverse=True + ) + percentages = levels.mapped("percentage") + level_percentage = list(zip(levels, percentages)) + for product_data in product_list: + product_data["value_percentage"] = ( + (100.0 * product_data["value"] / totals["value"]) + if totals["value"] + else 0.0 + ) + while ( + product_data["value_percentage"] < level_percentage[0][1] + and len(level_percentage) > 1 + ): + level_percentage.pop(0) + product_data["product"].abc_classification_level_id = level_percentage[ + 0 + ][0] diff --git a/product_abc_classification/models/product_category.py b/product_abc_classification/models/product_category.py new file mode 100644 index 00000000000..df7348e554c --- /dev/null +++ b/product_abc_classification/models/product_category.py @@ -0,0 +1,26 @@ +# Copyright 2020 ForgeFlow +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class ProductCategory(models.Model): + _inherit = "product.category" + + abc_classification_profile_id = fields.Many2one("abc.classification.profile") + product_variant_ids = fields.One2many("product.product", inverse_name="categ_id") + + @api.onchange("abc_classification_profile_id") + def _onchange_abc_classification_profile_id(self): + for categ in self: + for child in categ._origin.child_id: + child.abc_classification_profile_id = ( + categ.abc_classification_profile_id + ) + child._onchange_abc_classification_profile_id() + for variant in categ._origin.product_variant_ids.filtered( + lambda p: p.type == "product" + ): + variant.abc_classification_profile_id = ( + categ.abc_classification_profile_id + ) diff --git a/product_abc_classification/models/product_product.py b/product_abc_classification/models/product_product.py new file mode 100644 index 00000000000..99ce9470481 --- /dev/null +++ b/product_abc_classification/models/product_product.py @@ -0,0 +1,74 @@ +# Copyright 2020 ForgeFlow +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + abc_classification_profile_id = fields.Many2one( + "abc.classification.profile", + compute="_compute_abc_classification_profile_id", + inverse="_inverse_abc_classification_profile_id", + store=True, + ) + abc_classification_level_id = fields.Many2one( + "abc.classification.profile.level", + compute="_compute_abc_classification_level_id", + inverse="_inverse_abc_classification_level_id", + store=True, + ) + + @api.depends( + "product_variant_ids", "product_variant_ids.abc_classification_profile_id" + ) + def _compute_abc_classification_profile_id(self): + unique_variants = self.filtered( + lambda template: len(template.product_variant_ids) == 1 + ) + for template in unique_variants: + template.abc_classification_profile_id = ( + template.product_variant_ids.abc_classification_profile_id + ) + for template in self - unique_variants: + template.abc_classification_profile_id = False + + @api.depends( + "product_variant_ids", "product_variant_ids.abc_classification_level_id" + ) + def _compute_abc_classification_level_id(self): + unique_variants = self.filtered( + lambda template: len(template.product_variant_ids) == 1 + ) + for template in unique_variants: + template.abc_classification_level_id = ( + template.product_variant_ids.abc_classification_level_id + ) + for template in self - unique_variants: + template.abc_classification_level_id = False + + def _inverse_abc_classification_profile_id(self): + for template in self: + if len(template.product_variant_ids) == 1: + template.product_variant_ids.abc_classification_profile_id = ( + template.abc_classification_profile_id + ) + + def _inverse_abc_classification_level_id(self): + for template in self: + if len(template.product_variant_ids) == 1: + template.product_variant_ids.abc_classification_level_id = ( + template.abc_classification_level_id + ) + + +class ProductProduct(models.Model): + _inherit = "product.product" + + abc_classification_profile_id = fields.Many2one( + "abc.classification.profile", index=True + ) + abc_classification_level_id = fields.Many2one( + "abc.classification.profile.level", index=True + ) diff --git a/product_abc_classification/readme/CONTRIBUTORS.rst b/product_abc_classification/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..2e34e218a54 --- /dev/null +++ b/product_abc_classification/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Miquel Raïch diff --git a/product_abc_classification/readme/DESCRIPTION.rst b/product_abc_classification/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..b55e108765f --- /dev/null +++ b/product_abc_classification/readme/DESCRIPTION.rst @@ -0,0 +1,8 @@ +This modules includes the ABC analysis (or ABC classification), which is +used by inventory management teams to help identify the most important +products in their portfolio and ensure they prioritize managing them above +those less valuable. + +Managers will create a profile with several levels (percentages) and then the +profiled products will automatically get a corresponding level using the +ABC classification. diff --git a/product_abc_classification/readme/USAGE.rst b/product_abc_classification/readme/USAGE.rst new file mode 100644 index 00000000000..a66526e486c --- /dev/null +++ b/product_abc_classification/readme/USAGE.rst @@ -0,0 +1,11 @@ +To use this module, you need to: + +#. Go to Sales or Inventory menu, then to Configuration/Products/ABC Classification Profile +and create a profile with levels, knowing that the sum of all levels in the profile +should sum 100 and all the levels should be different. + +#. Later you should go to product categories or product variants, and assign them a profile. +Then the cron classification will proceed to assign to these products one of the profile's levels. + +NOTE: If you profile (or unprofile) a product category, then all its +child categories and products will be profiled (or unprofiled). diff --git a/product_abc_classification/security/ir.model.access.csv b/product_abc_classification/security/ir.model.access.csv new file mode 100644 index 00000000000..421beb328a9 --- /dev/null +++ b/product_abc_classification/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_abc_classification_profile_user,abc.classification.profile.user,model_abc_classification_profile,base.group_user,1,0,0,0 +access_abc_classification_profile_manager,abc.classification.profile.manager,model_abc_classification_profile,base.group_system,1,1,1,1 +access_abc_classification_profile_level_user,abc.classification.profile.level.user,model_abc_classification_profile_level,base.group_user,1,0,0,0 +access_abc_classification_profile_level_manager,abc.classification.profile.level.manager,model_abc_classification_profile_level,base.group_system,1,1,1,1 diff --git a/product_abc_classification/static/description/icon.png b/product_abc_classification/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/product_abc_classification/views/abc_classification_view.xml b/product_abc_classification/views/abc_classification_view.xml new file mode 100644 index 00000000000..001fd3b21e6 --- /dev/null +++ b/product_abc_classification/views/abc_classification_view.xml @@ -0,0 +1,69 @@ + + + + + abc.classification.profile.form + abc.classification.profile + +
+ + + + + + + + + + + + + + + + + +
+
+
+ + abc.classification.profile.tree + abc.classification.profile + + + + + + + + + ABC Classification Profile + abc.classification.profile + tree,form + +

+ Click to add a new profile. +

+

+ This allows to create an ABC classification. +

+
+
+ + +
diff --git a/product_abc_classification/views/product_view.xml b/product_abc_classification/views/product_view.xml new file mode 100644 index 00000000000..5caf6a28283 --- /dev/null +++ b/product_abc_classification/views/product_view.xml @@ -0,0 +1,94 @@ + + + + + + product.template.search (ABC Classification) + product.template + + + + + + + + + + product.template.tree + product.template + + + + + + + + + + product.product.tree + product.product + + + + + + + + + + product.template.form (ABC Classification) + product.template + + + + + + + + + + + + + + + product.category.form (ABC Classification) + product.category + + + + + + + + From 5a63ff452b462c443f4cbb095372437312b86e76 Mon Sep 17 00:00:00 2001 From: Lindsay Marion Date: Tue, 26 Jan 2021 12:08:02 +0100 Subject: [PATCH 02/22] [IMP] product_abc_classification: Makes the classification computation logic pluggable --- product_abc_classification/__init__.py | 1 - product_abc_classification/__manifest__.py | 20 --- product_abc_classification/data/ir_cron.xml | 13 -- product_abc_classification/models/__init__.py | 4 - .../models/abc_classification_level.py | 35 ---- .../models/abc_classification_profile.py | 156 ------------------ .../models/product_category.py | 26 --- .../models/product_product.py | 74 --------- .../readme/CONTRIBUTORS.rst | 1 - .../readme/DESCRIPTION.rst | 8 - product_abc_classification/readme/USAGE.rst | 11 -- .../security/ir.model.access.csv | 5 - .../static/description/icon.png | Bin 9455 -> 0 bytes .../views/abc_classification_view.xml | 69 -------- .../views/product_view.xml | 94 ----------- 15 files changed, 517 deletions(-) delete mode 100644 product_abc_classification/__init__.py delete mode 100644 product_abc_classification/__manifest__.py delete mode 100644 product_abc_classification/data/ir_cron.xml delete mode 100644 product_abc_classification/models/__init__.py delete mode 100644 product_abc_classification/models/abc_classification_level.py delete mode 100644 product_abc_classification/models/abc_classification_profile.py delete mode 100644 product_abc_classification/models/product_category.py delete mode 100644 product_abc_classification/models/product_product.py delete mode 100644 product_abc_classification/readme/CONTRIBUTORS.rst delete mode 100644 product_abc_classification/readme/DESCRIPTION.rst delete mode 100644 product_abc_classification/readme/USAGE.rst delete mode 100644 product_abc_classification/security/ir.model.access.csv delete mode 100644 product_abc_classification/static/description/icon.png delete mode 100644 product_abc_classification/views/abc_classification_view.xml delete mode 100644 product_abc_classification/views/product_view.xml diff --git a/product_abc_classification/__init__.py b/product_abc_classification/__init__.py deleted file mode 100644 index 0650744f6bc..00000000000 --- a/product_abc_classification/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import models diff --git a/product_abc_classification/__manifest__.py b/product_abc_classification/__manifest__.py deleted file mode 100644 index d67fd6bc574..00000000000 --- a/product_abc_classification/__manifest__.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2020 ForgeFlow -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). -{ - "name": "Product ABC Classification", - "summary": "Includes ABC classification for inventory management", - "version": "13.0.1.0.0", - "author": "ForgeFlow, Odoo Community Association (OCA)", - "website": "https://github.com/OCA/product-attribute", - "category": "Inventory Management", - "license": "AGPL-3", - "maintainers": ["MiquelRForgeFlow"], - "depends": ["sale_stock"], - "data": [ - "security/ir.model.access.csv", - "views/product_view.xml", - "views/abc_classification_view.xml", - "data/ir_cron.xml", - ], - "installable": True, -} diff --git a/product_abc_classification/data/ir_cron.xml b/product_abc_classification/data/ir_cron.xml deleted file mode 100644 index 36faa7971a5..00000000000 --- a/product_abc_classification/data/ir_cron.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - Perform the product ABC Classification - 1 - days - -1 - - - model._compute_abc_classification() - code - - diff --git a/product_abc_classification/models/__init__.py b/product_abc_classification/models/__init__.py deleted file mode 100644 index 39394627c87..00000000000 --- a/product_abc_classification/models/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from . import product_category -from . import product_product -from . import abc_classification_level -from . import abc_classification_profile diff --git a/product_abc_classification/models/abc_classification_level.py b/product_abc_classification/models/abc_classification_level.py deleted file mode 100644 index 2b6790c6b5b..00000000000 --- a/product_abc_classification/models/abc_classification_level.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2020 ForgeFlow -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). - -from odoo import _, api, fields, models -from odoo.exceptions import ValidationError - - -class ABCClasificationProfileLevel(models.Model): - _name = "abc.classification.profile.level" - _description = "ABC Clasification Profile Level" - _order = "percentage desc, id desc" - - percentage = fields.Float(default=0.0, required=True, string="%") - profile_id = fields.Many2one("abc.classification.profile") - - def name_get(self): - def _get_sort_key_percentage(rec): - return rec.percentage - - res = [] - for profile in self.mapped("profile_id"): - for i, level in enumerate( - profile.level_ids.sorted(key=_get_sort_key_percentage, reverse=True) - ): - name = "{} ({}%)".format(chr(65 + i), level.percentage) - res += [(level.id, name)] - return res - - @api.constrains("percentage") - def _check_percentage(self): - for level in self: - if level.percentage > 100.0: - raise ValidationError(_("The percentage cannot be greater than 100.")) - elif level.percentage <= 0.0: - raise ValidationError(_("The percentage should be a positive number.")) diff --git a/product_abc_classification/models/abc_classification_profile.py b/product_abc_classification/models/abc_classification_profile.py deleted file mode 100644 index f75be657558..00000000000 --- a/product_abc_classification/models/abc_classification_profile.py +++ /dev/null @@ -1,156 +0,0 @@ -# Copyright 2020 ForgeFlow -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). -from datetime import timedelta - -from odoo import _, api, fields, models -from odoo.exceptions import ValidationError - - -class ABCClasificationProfile(models.Model): - _name = "abc.classification.profile" - _description = "ABC Clasification Profile" - - name = fields.Char() - level_ids = fields.One2many( - comodel_name="abc.classification.profile.level", inverse_name="profile_id" - ) - representation = fields.Char(compute="_compute_representation") - data_source = fields.Selection( - selection=[("stock_moves", "Stock Moves")], - default="stock_moves", - string="Data Source", - index=True, - required=True, - ) - value_criteria = fields.Selection( - selection=[("consumption_value", "Consumption Value")], - # others: 'sales revenue', 'profitability', ... - default="consumption_value", - string="Value", - index=True, - required=True, - ) - past_period = fields.Integer( - default=365, string="Past demand period (Days)", required=True - ) - - @api.depends("level_ids") - def _compute_representation(self): - def _get_sort_key_percentage(rec): - return rec.percentage - - for profile in self: - profile.level_ids.sorted(key=_get_sort_key_percentage, reverse=True) - profile.representation = "/".join( - [str(x) for x in profile.level_ids.mapped("display_name")] - ) - - @api.constrains("level_ids") - def _check_levels(self): - for profile in self: - percentages = profile.level_ids.mapped("percentage") - total = sum(percentages) - if profile.level_ids and total != 100.0: - raise ValidationError( - _("The sum of the percentages of the levels should be 100.") - ) - if profile.level_ids and len({}.fromkeys(percentages)) != len(percentages): - raise ValidationError( - _("The percentages of the levels must be unique.") - ) - - def write(self, vals): - return super().write(vals) - - def _fill_initial_product_data(self, date): - product_list = [] - if self.data_source == "stock_moves": - return self._fill_data_from_stock_moves(date, product_list) - else: - return product_list - - def _fill_data_from_stock_moves(self, date, product_list): - self.ensure_one() - moves = ( - self.env["stock.move"] - .sudo() - .read_group( - [ - ("state", "=", "done"), - ("date", ">", date), - ("location_dest_id.usage", "=", "customer"), - ("location_id.usage", "!=", "customer"), - ("product_id.type", "=", "product"), - "|", - ("product_id.abc_classification_profile_id", "=", self.id), - "|", - ("product_id.categ_id.abc_classification_profile_id", "=", self.id), - ( - "product_id.categ_id.parent_id.abc_classification_profile_id", - "=", - self.id, - ), - ], - ["product_id", "product_qty"], - ["product_id"], - ) - ) - for move in moves: - product_data = { - "product": self.env["product.product"].browse(move["product_id"][0]), - "units_sold": move["product_qty"], - } - product_list.append(product_data) - return product_list - - def _get_inventory_product_value(self, data): - self.ensure_one() - if self.value_criteria == "consumption_value": - return data["unit_cost"] * data["units_sold"] - raise 0.0 - - @api.model - def _compute_abc_classification(self): - def _get_sort_key_value(data): - return data["value"] - - def _get_sort_key_percentage(rec): - return rec.percentage - - profiles = self.search([]).filtered(lambda p: p.level_ids) - for profile in profiles: - oldest_date = fields.Datetime.to_string( - fields.Datetime.today() - timedelta(days=profile.past_period) - ) - totals = { - "units_sold": 0, - "value": 0.0, - } - product_list = profile._fill_initial_product_data(oldest_date) - for product_data in product_list: - product_data["unit_cost"] = product_data["product"].standard_price - totals["units_sold"] += product_data["units_sold"] - product_data["value"] = profile._get_inventory_product_value( - product_data - ) - totals["value"] += product_data["value"] - product_list.sort(reverse=True, key=_get_sort_key_value) - levels = profile.level_ids.sorted( - key=_get_sort_key_percentage, reverse=True - ) - percentages = levels.mapped("percentage") - level_percentage = list(zip(levels, percentages)) - for product_data in product_list: - product_data["value_percentage"] = ( - (100.0 * product_data["value"] / totals["value"]) - if totals["value"] - else 0.0 - ) - while ( - product_data["value_percentage"] < level_percentage[0][1] - and len(level_percentage) > 1 - ): - level_percentage.pop(0) - product_data["product"].abc_classification_level_id = level_percentage[ - 0 - ][0] diff --git a/product_abc_classification/models/product_category.py b/product_abc_classification/models/product_category.py deleted file mode 100644 index df7348e554c..00000000000 --- a/product_abc_classification/models/product_category.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2020 ForgeFlow -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). - -from odoo import api, fields, models - - -class ProductCategory(models.Model): - _inherit = "product.category" - - abc_classification_profile_id = fields.Many2one("abc.classification.profile") - product_variant_ids = fields.One2many("product.product", inverse_name="categ_id") - - @api.onchange("abc_classification_profile_id") - def _onchange_abc_classification_profile_id(self): - for categ in self: - for child in categ._origin.child_id: - child.abc_classification_profile_id = ( - categ.abc_classification_profile_id - ) - child._onchange_abc_classification_profile_id() - for variant in categ._origin.product_variant_ids.filtered( - lambda p: p.type == "product" - ): - variant.abc_classification_profile_id = ( - categ.abc_classification_profile_id - ) diff --git a/product_abc_classification/models/product_product.py b/product_abc_classification/models/product_product.py deleted file mode 100644 index 99ce9470481..00000000000 --- a/product_abc_classification/models/product_product.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2020 ForgeFlow -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). - -from odoo import api, fields, models - - -class ProductTemplate(models.Model): - _inherit = "product.template" - - abc_classification_profile_id = fields.Many2one( - "abc.classification.profile", - compute="_compute_abc_classification_profile_id", - inverse="_inverse_abc_classification_profile_id", - store=True, - ) - abc_classification_level_id = fields.Many2one( - "abc.classification.profile.level", - compute="_compute_abc_classification_level_id", - inverse="_inverse_abc_classification_level_id", - store=True, - ) - - @api.depends( - "product_variant_ids", "product_variant_ids.abc_classification_profile_id" - ) - def _compute_abc_classification_profile_id(self): - unique_variants = self.filtered( - lambda template: len(template.product_variant_ids) == 1 - ) - for template in unique_variants: - template.abc_classification_profile_id = ( - template.product_variant_ids.abc_classification_profile_id - ) - for template in self - unique_variants: - template.abc_classification_profile_id = False - - @api.depends( - "product_variant_ids", "product_variant_ids.abc_classification_level_id" - ) - def _compute_abc_classification_level_id(self): - unique_variants = self.filtered( - lambda template: len(template.product_variant_ids) == 1 - ) - for template in unique_variants: - template.abc_classification_level_id = ( - template.product_variant_ids.abc_classification_level_id - ) - for template in self - unique_variants: - template.abc_classification_level_id = False - - def _inverse_abc_classification_profile_id(self): - for template in self: - if len(template.product_variant_ids) == 1: - template.product_variant_ids.abc_classification_profile_id = ( - template.abc_classification_profile_id - ) - - def _inverse_abc_classification_level_id(self): - for template in self: - if len(template.product_variant_ids) == 1: - template.product_variant_ids.abc_classification_level_id = ( - template.abc_classification_level_id - ) - - -class ProductProduct(models.Model): - _inherit = "product.product" - - abc_classification_profile_id = fields.Many2one( - "abc.classification.profile", index=True - ) - abc_classification_level_id = fields.Many2one( - "abc.classification.profile.level", index=True - ) diff --git a/product_abc_classification/readme/CONTRIBUTORS.rst b/product_abc_classification/readme/CONTRIBUTORS.rst deleted file mode 100644 index 2e34e218a54..00000000000 --- a/product_abc_classification/readme/CONTRIBUTORS.rst +++ /dev/null @@ -1 +0,0 @@ -* Miquel Raïch diff --git a/product_abc_classification/readme/DESCRIPTION.rst b/product_abc_classification/readme/DESCRIPTION.rst deleted file mode 100644 index b55e108765f..00000000000 --- a/product_abc_classification/readme/DESCRIPTION.rst +++ /dev/null @@ -1,8 +0,0 @@ -This modules includes the ABC analysis (or ABC classification), which is -used by inventory management teams to help identify the most important -products in their portfolio and ensure they prioritize managing them above -those less valuable. - -Managers will create a profile with several levels (percentages) and then the -profiled products will automatically get a corresponding level using the -ABC classification. diff --git a/product_abc_classification/readme/USAGE.rst b/product_abc_classification/readme/USAGE.rst deleted file mode 100644 index a66526e486c..00000000000 --- a/product_abc_classification/readme/USAGE.rst +++ /dev/null @@ -1,11 +0,0 @@ -To use this module, you need to: - -#. Go to Sales or Inventory menu, then to Configuration/Products/ABC Classification Profile -and create a profile with levels, knowing that the sum of all levels in the profile -should sum 100 and all the levels should be different. - -#. Later you should go to product categories or product variants, and assign them a profile. -Then the cron classification will proceed to assign to these products one of the profile's levels. - -NOTE: If you profile (or unprofile) a product category, then all its -child categories and products will be profiled (or unprofiled). diff --git a/product_abc_classification/security/ir.model.access.csv b/product_abc_classification/security/ir.model.access.csv deleted file mode 100644 index 421beb328a9..00000000000 --- a/product_abc_classification/security/ir.model.access.csv +++ /dev/null @@ -1,5 +0,0 @@ -id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_abc_classification_profile_user,abc.classification.profile.user,model_abc_classification_profile,base.group_user,1,0,0,0 -access_abc_classification_profile_manager,abc.classification.profile.manager,model_abc_classification_profile,base.group_system,1,1,1,1 -access_abc_classification_profile_level_user,abc.classification.profile.level.user,model_abc_classification_profile_level,base.group_user,1,0,0,0 -access_abc_classification_profile_level_manager,abc.classification.profile.level.manager,model_abc_classification_profile_level,base.group_system,1,1,1,1 diff --git a/product_abc_classification/static/description/icon.png b/product_abc_classification/static/description/icon.png deleted file mode 100644 index 3a0328b516c4980e8e44cdb63fd945757ddd132d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I diff --git a/product_abc_classification/views/abc_classification_view.xml b/product_abc_classification/views/abc_classification_view.xml deleted file mode 100644 index 001fd3b21e6..00000000000 --- a/product_abc_classification/views/abc_classification_view.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - abc.classification.profile.form - abc.classification.profile - -
- - - - - - - - - - - - - - - - - -
-
-
- - abc.classification.profile.tree - abc.classification.profile - - - - - - - - - ABC Classification Profile - abc.classification.profile - tree,form - -

- Click to add a new profile. -

-

- This allows to create an ABC classification. -

-
-
- - -
diff --git a/product_abc_classification/views/product_view.xml b/product_abc_classification/views/product_view.xml deleted file mode 100644 index 5caf6a28283..00000000000 --- a/product_abc_classification/views/product_view.xml +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - product.template.search (ABC Classification) - product.template - - - - - - - - - - product.template.tree - product.template - - - - - - - - - - product.product.tree - product.product - - - - - - - - - - product.template.form (ABC Classification) - product.template - - - - - - - - - - - - - - - product.category.form (ABC Classification) - product.category - - - - - - - - From 0cfc4b5450b237e719515f7d1899e6129968dfc4 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Thu, 10 Nov 2022 14:21:24 +0100 Subject: [PATCH 03/22] [IMP] product_abc_classification/base: Restore module name --- product_abc_classification/README.rst | 102 ++++ product_abc_classification/__init__.py | 1 + product_abc_classification/__manifest__.py | 22 + product_abc_classification/data/ir_cron.xml | 14 + product_abc_classification/i18n/fr.po | 327 +++++++++++++ product_abc_classification/models/__init__.py | 5 + .../models/abc_classification_level.py | 48 ++ .../abc_classification_product_level.py | 159 +++++++ .../models/abc_classification_profile.py | 96 ++++ .../models/product_product.py | 21 + .../models/product_template.py | 70 +++ .../readme/CONTRIBUTORS.rst | 4 + .../readme/DESCRIPTION.rst | 11 + product_abc_classification/readme/USAGE.rst | 11 + .../security/ir.model.access.csv | 7 + .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 443 ++++++++++++++++++ product_abc_classification/tests/__init__.py | 3 + product_abc_classification/tests/common.py | 123 +++++ .../test_abc_classification_product_level.py | 363 ++++++++++++++ .../tests/test_abc_classification_profile.py | 300 ++++++++++++ .../tests/test_product.py | 109 +++++ .../abc_classification_product_level.xml | 101 ++++ .../views/abc_classification_profile.xml | 54 +++ .../views/product_product.xml | 19 + .../views/product_template.xml | 32 ++ 26 files changed, 2445 insertions(+) create mode 100644 product_abc_classification/README.rst create mode 100644 product_abc_classification/__init__.py create mode 100644 product_abc_classification/__manifest__.py create mode 100644 product_abc_classification/data/ir_cron.xml create mode 100644 product_abc_classification/i18n/fr.po create mode 100644 product_abc_classification/models/__init__.py create mode 100644 product_abc_classification/models/abc_classification_level.py create mode 100644 product_abc_classification/models/abc_classification_product_level.py create mode 100644 product_abc_classification/models/abc_classification_profile.py create mode 100644 product_abc_classification/models/product_product.py create mode 100644 product_abc_classification/models/product_template.py create mode 100644 product_abc_classification/readme/CONTRIBUTORS.rst create mode 100644 product_abc_classification/readme/DESCRIPTION.rst create mode 100644 product_abc_classification/readme/USAGE.rst create mode 100644 product_abc_classification/security/ir.model.access.csv create mode 100644 product_abc_classification/static/description/icon.png create mode 100644 product_abc_classification/static/description/index.html create mode 100644 product_abc_classification/tests/__init__.py create mode 100644 product_abc_classification/tests/common.py create mode 100644 product_abc_classification/tests/test_abc_classification_product_level.py create mode 100644 product_abc_classification/tests/test_abc_classification_profile.py create mode 100644 product_abc_classification/tests/test_product.py create mode 100644 product_abc_classification/views/abc_classification_product_level.xml create mode 100644 product_abc_classification/views/abc_classification_profile.xml create mode 100644 product_abc_classification/views/product_product.xml create mode 100644 product_abc_classification/views/product_template.xml diff --git a/product_abc_classification/README.rst b/product_abc_classification/README.rst new file mode 100644 index 00000000000..bbed589ebf5 --- /dev/null +++ b/product_abc_classification/README.rst @@ -0,0 +1,102 @@ +========================== +Product Abc Classification +========================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! 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-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 + :target: https://github.com/OCA/product-attribute/tree/16.0/product_abc_classification_base + :alt: OCA/product-attribute +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/product-attribute-16-0/product-attribute-16-0-product_abc_classification_base + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/135/16.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +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 portfolio and ensure they +prioritize managing them above those less valuable. + +Managers will create a profile with several levels (percentages) and then the +profiled products will automatically get a corresponding level using the +ABC classification. + +The addon *product_abc_classification_sale_stock* defines a computation profile +based on the number of sale order line delivered by product. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To use this module, you need to: + +#. Go to Sales or Inventory menu, then to Configuration/Products/ABC Classification Profile +and create a profile with levels, knowing that the sum of all levels in the profile +should sum 100 and all the levels should be different. + +#. Later you should go to product categories or product variants, and assign them a profile. +Then the cron classification will proceed to assign to these products one of the profile's levels. + +NOTE: If you profile (or unprofile) a product category, then all its +child categories and products will be profiled (or unprofiled). + +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 +~~~~~~~ + +* ACSONE SA/NV +* ForgeFlow + +Contributors +~~~~~~~~~~~~ + +* Miquel Raïch +* Lindsay Marion +* Laurent Mignon +* Denis Roussel + +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/product-attribute `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_abc_classification/__init__.py b/product_abc_classification/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/product_abc_classification/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/product_abc_classification/__manifest__.py b/product_abc_classification/__manifest__.py new file mode 100644 index 00000000000..310fe0ad734 --- /dev/null +++ b/product_abc_classification/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2020 ForgeFlow +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Product Abc Classification", + "summary": """ + ABC classification for sales and warehouse management""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV, ForgeFlow, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/product-attribute", + "depends": ["product", "stock"], + "data": [ + "views/abc_classification_product_level.xml", + "views/abc_classification_profile.xml", + "views/product_template.xml", + "views/product_product.xml", + "security/ir.model.access.csv", + "data/ir_cron.xml", + ], +} diff --git a/product_abc_classification/data/ir_cron.xml b/product_abc_classification/data/ir_cron.xml new file mode 100644 index 00000000000..04303328c19 --- /dev/null +++ b/product_abc_classification/data/ir_cron.xml @@ -0,0 +1,14 @@ + + + + Perform the product ABC Classification + + 1 + months + -1 + + + model._cron_compute_abc_classification() + code + + diff --git a/product_abc_classification/i18n/fr.po b/product_abc_classification/i18n/fr.po new file mode 100644 index 00000000000..47bc99abc3f --- /dev/null +++ b/product_abc_classification/i18n/fr.po @@ -0,0 +1,327 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_abc_classification +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-02-15 16:46+0000\n" +"PO-Revision-Date: 2021-02-15 16:46+0000\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: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_percentage +msgid "% Indicator" +msgstr "% KPI + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_percentage_products +msgid "% Products" +msgstr "% Articles" + +#. module: product_abc_classification +#: model:ir.ui.view,arch_db:product_abc_classification.product_template_form_view +msgid "ABC Classification" +msgstr "Classification ABC" + +#. module: product_abc_classification +#: model:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_form_view +msgid "ABC Classification Product Level" +msgstr "Niveau de classification ABC des articles" + +#. module: product_abc_classification +#: model:ir.actions.act_window,name:product_abc_classification.abc_classification_profile_action +#: model:ir.ui.menu,name:product_abc_classification.menu_abc_classification_profile_config_stock +msgid "ABC Classification profiles" +msgstr "Profils de classification ABC" + +#. module: product_abc_classification +#: model:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_form_view +msgid "ABC Profile" +msgstr "Profil ABC" + +#. module: product_abc_classification +#: model:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_tree_view +msgid "ABC Profiles" +msgstr "Profils ABC" + +#. module: product_abc_classification +#: model:ir.model,name:product_abc_classification.model_abc_classification_product_level +msgid "Abc Classification Product Level" +msgstr "Niveau de classification" + +#. module: product_abc_classification +#: model:ir.model,name:product_abc_classification.model_abc_classification_profile +msgid "Abc Classification Profile" +msgstr "Profil de classification ABC" + +#. module: product_abc_classification +#: model:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view +msgid "Abc classification" +msgstr "Classification ABC" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_delivery_carrier_abc_classification_product_level_ids +#: model:ir.model.fields,field_description:product_abc_classification.field_product_product_abc_classification_product_level_ids +#: model:ir.model.fields,field_description:product_abc_classification.field_product_template_abc_classification_product_level_ids +msgid "Abc classification product level ids" +msgstr "Classes ABC" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_allowed_profile_ids +#: model:ir.model.fields,field_description:product_abc_classification.field_delivery_carrier_abc_classification_profile_ids +#: model:ir.model.fields,field_description:product_abc_classification.field_product_product_abc_classification_profile_ids +#: model:ir.model.fields,field_description:product_abc_classification.field_product_template_abc_classification_profile_ids +msgid "Abc classification profile ids" +msgstr "Profils ABC" + +#. module: product_abc_classification +#: selection:abc.classification.profile,profile_type:0 +msgid "Based on the count of delivered sale order line by product" +msgstr "Basé sur le total des lignes de vente par article" + +#. module: product_abc_classification +#: model:ir.model.fields,help:product_abc_classification.field_abc_classification_level_name +msgid "Classification A, B or C" +msgstr "Classification A, B ou C" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_level_id +msgid "Classification level" +msgstr "Classe / Niveau" + +#. module: product_abc_classification +#: code:addons/product_abc_classification/models/abc_classification_product_level.py:84 +#, python-format +msgid "Classification level is mandatory" +msgstr "La classe / niveau est obligatoire" + +#. module: product_abc_classification +#: model:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view +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_computed_level_id +msgid "Computed classification level" +msgstr "Classe calculée" + +#. module: product_abc_classification +#: model:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_form_view +msgid "Computed level differs from the specified level" +msgstr "La class calculée diverge de la valeur spécifiée" + +#. module: product_abc_classification +#: code:addons/product_abc_classification/models/abc_classification_product_level.py:90 +#, python-format +msgid "Computed level must be in the same classifiation profile as the one on the product level" +msgstr "La classe calculée doit utiliser le même profil de classification que celui défini sur le produit" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_create_uid +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_create_uid +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_create_uid +msgid "Created by" +msgstr "Créé par" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_create_date +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_create_date +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_create_date +msgid "Created on" +msgstr "Créé le" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_display_name +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_display_name +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: product_abc_classification +#: model:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view +msgid "Group By" +msgstr "Grouper par" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_id +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_id +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_id +msgid "ID" +msgstr "ID" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_flag +msgid "If True, this means that the manual classification is different from the computed one" +msgstr "Si coché, indique que la classe attribuée manuellement au produit diverge de la classe calculée par le système." + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level___last_update +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level___last_update +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile___last_update +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_write_uid +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_write_uid +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_write_uid +msgid "Last Updated by" +msgstr "Dernière mise à jour par" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_write_date +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_write_date +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_write_date +msgid "Last Updated on" +msgstr "Dernière mise à jour le" + +#. module: product_abc_classification +#: model:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view +msgid "Level" +msgstr "Niveau" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_level_ids +msgid "Level ids" +msgstr "Classes" + +#. module: product_abc_classification +#: sql_constraint:abc.classification.level:0 +#: code:addons/product_abc_classification/models/abc_classification_level.py:30 +#, python-format +msgid "Level name must be unique by profile" +msgstr "Le nom de la classe doit être unique par profil" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_manual_level_id +msgid "Manual classification level" +msgstr "Classe (Valeur à utiliser)" + +#. module: product_abc_classification +#: code:addons/product_abc_classification/models/abc_classification_product_level.py:100 +#, python-format +msgid "Manual level must be in the same classifiation profile as the one on the product level" +msgstr "La classe à utiliser doit utiliser le même profil de classification que celui défini sur le produit" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_name +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_name +msgid "Name" +msgstr "Nom" + +#. module: product_abc_classification +#: sql_constraint:abc.classification.product.level:0 +#: code:addons/product_abc_classification/models/abc_classification_product_level.py:76 +#, python-format +msgid "Only one level by profile by product allowed" +msgstr "Une classe de classification ABC par profil et par produit autorisée." + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_period +msgid "Period on which to compute the classification (Days)" +msgstr "Période référence pour le calcul de la classification (Nbr jours)" + +#. module: product_abc_classification +#: model:ir.model,name:product_abc_classification.model_product_product +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_product_id +msgid "Product" +msgstr "Article" + +#. module: product_abc_classification +#: model:ir.model,name:product_abc_classification.model_product_template +msgid "Product Template" +msgstr "Modèle de produit" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_product_tmpl_id +msgid "Product template" +msgstr "Modèle de produit" + +#. module: product_abc_classification +#: model:ir.actions.act_window,name:product_abc_classification.abc_classification_product_level_action +#: model:ir.ui.menu,name:product_abc_classification.menu_abc_classification_product_level_config_stock +msgid "Products ABC Classification" +msgstr "Classification ABC des articles" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_profile_id +#: model:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view +msgid "Profile" +msgstr "Profil" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_profile_id +msgid "Profile id" +msgstr "Profil" + +#. module: product_abc_classification +#: sql_constraint:abc.classification.profile:0 +#: code:addons/product_abc_classification/models/abc_classification_profile.py:33 +#, python-format +msgid "Profile name must be unique" +msgstr "Le nom du profil doit être unique" + +#. module: product_abc_classification +#: code:addons/product_abc_classification/models/abc_classification_level.py:39 +#, python-format +msgid "The percentage cannot be greater than 100." +msgstr "Le pourcentage ne peut pas dépasser 100." + +#. module: product_abc_classification +#: code:addons/product_abc_classification/models/abc_classification_level.py:51 +#, python-format +msgid "The percentage of products cannot be greater than 100." +msgstr "Le pourcentage d'articles' ne peut pas dépasser 100." + +#. module: product_abc_classification +#: code:addons/product_abc_classification/models/abc_classification_level.py:55 +#, python-format +msgid "The percentage of products should be a positive number." +msgstr "Le pourcentage d'articles' doit être un nombre positif." + +#. module: product_abc_classification +#: code:addons/product_abc_classification/models/abc_classification_level.py:43 +#, python-format +msgid "The percentage should be a positive number." +msgstr "Le pourcentage doit être un nombre positif." + +#. module: product_abc_classification +#: code:addons/product_abc_classification/models/abc_classification_profile.py:52 +#, python-format +msgid "The percentages of the levels must be unique." +msgstr "Les valeurs de pourcentage des différentes classes doivent être uniques pour un même profil." + +#. module: product_abc_classification +#: code:addons/product_abc_classification/models/abc_classification_profile.py:43 +#, python-format +msgid "The sum of the percentages of the levels should be 100." +msgstr "La somme des pourcentages ne doit pas dépasser 100." + +#. module: product_abc_classification +#: code:addons/product_abc_classification/models/abc_classification_profile.py:60 +#, python-format +msgid "The sum of the products percentages of the levels should be 100." +msgstr "La somme des pourcentages d'articles ne doit pas dépasser 100." + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_profile_type +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_profile_type +msgid "Type of ABC classification" +msgstr "Type de classification ABC" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_auto_apply_computed_value +msgid "Auto apply computed value" +msgstr "Appliquer automatiquement la classification calculée" + +#. module: product_abc_classification +#: model:ir.model,name:product_abc_classification.model_abc_classification_level +msgid "abc.classification.level" +msgstr "Classe de classification ABC" diff --git a/product_abc_classification/models/__init__.py b/product_abc_classification/models/__init__.py new file mode 100644 index 00000000000..b98adc64bd4 --- /dev/null +++ b/product_abc_classification/models/__init__.py @@ -0,0 +1,5 @@ +from . import abc_classification_profile +from . import abc_classification_level +from . import product_template +from . import product_product +from . import abc_classification_product_level diff --git a/product_abc_classification/models/abc_classification_level.py b/product_abc_classification/models/abc_classification_level.py new file mode 100644 index 00000000000..01b045eabc7 --- /dev/null +++ b/product_abc_classification/models/abc_classification_level.py @@ -0,0 +1,48 @@ +# Copyright 2020 ForgeFlow +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class AbcClassificationLevel(models.Model): + + _name = "abc.classification.level" + _description = "ABC Classification Level" + _order = "percentage desc, id desc" + _rec_name = "name" + + percentage_products = fields.Float(default=0.0, required=True, string="% Products") + percentage = fields.Float(default=0.0, required=True, string="% Indicator") + profile_id = fields.Many2one("abc.classification.profile", ondelete="cascade") + + name = fields.Char(help="Classification A, B or C", required=True) + + _sql_constraints = [ + ( + "name_uniq", + "UNIQUE(profile_id, name)", + _("Level name must be unique by profile"), + ) + ] + + @api.constrains("percentage") + def _check_percentage(self): + for level in self: + if level.percentage > 100.0: + raise ValidationError(_("The percentage cannot be greater than 100.")) + if level.percentage <= 0.0: + raise ValidationError(_("The percentage should be a positive number.")) + + @api.constrains("percentage_products") + def _check_percentage_products(self): + for level in self: + if level.percentage_products > 100.0: + raise ValidationError( + _("The percentage of products cannot be greater than 100.") + ) + if level.percentage_products <= 0.0: + raise ValidationError( + _("The percentage of products should be a positive number.") + ) diff --git a/product_abc_classification/models/abc_classification_product_level.py b/product_abc_classification/models/abc_classification_product_level.py new file mode 100644 index 00000000000..1ad9cf6c184 --- /dev/null +++ b/product_abc_classification/models/abc_classification_product_level.py @@ -0,0 +1,159 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class AbcClassificationProductLevel(models.Model): + _name = "abc.classification.product.level" + _inherit = "mail.thread" + _description = "Abc Classification Product Level" + _rec_name = "level_id" + + display_name = fields.Char(compute="_compute_display_name") + + manual_level_id = fields.Many2one( + "abc.classification.level", + string="Manual classification level", + tracking=True, + domain="[('profile_id', '=', profile_id)]", + ) + computed_level_id = fields.Many2one( + "abc.classification.level", + string="Computed classification level", + readonly=True, + ) + level_id = fields.Many2one( + "abc.classification.level", + string="Classification level", + compute="_compute_level_id", + store=True, + domain="[('profile_id', '=', profile_id)]", + ) + flag = fields.Boolean( + default=False, + compute="_compute_flag", + string="If True, this means that the manual classification is " + "different from the computed one", + store=True, + index=True, + ) + product_id = fields.Many2one( + "product.product", + string="Product", + index=True, + required=True, + ondelete="cascade", + ) + product_tmpl_id = fields.Many2one( + "product.template", + string="Product template", + index=True, + readonly=True, + ) + # percentage + profile_id = fields.Many2one( + "abc.classification.profile", + string="Profile", + required=True, + ) + profile_type = fields.Selection( + related="profile_id.profile_type", + readonly=True, + store=True, + ) + allowed_profile_ids = fields.Many2many( + comodel_name="abc.classification.profile", + related="product_id.abc_classification_profile_ids", + ) + + _sql_constraints = [ + ( + "product_level_uniq", + "UNIQUE(profile_id, product_id)", + _("Only one level by profile by product allowed"), + ) + ] + + @api.constrains("computed_level_id", "manual_level_id", "product_id") + def _check_level(self): + for rec in self: + if not rec.computed_level_id and not rec.manual_level_id: + raise ValidationError(_("Classification level is mandatory")) + if ( + rec.computed_level_id + and rec.computed_level_id.profile_id != rec.profile_id + ): + raise ValidationError( + _( + "Computed level must be in the same classifiation " + "profile as the one on the product level" + ) + ) + if rec.manual_level_id and rec.manual_level_id.profile_id != rec.profile_id: + raise ValidationError( + _( + "Manual level must be in the same classifiation " + "profile as the one on the product level" + ) + ) + + @api.onchange("product_tmpl_id") + def _onchange_product_tmpl_id(self): + for rec in self.filtered( + lambda a: a.product_tmpl_id.product_variant_count == 1 + ): + rec.product_id = rec.product_tmpl_id.product_variant_id + + @api.depends("level_id", "profile_id") + def _compute_display_name(self): + for record in self: + record.display_name = "{profile_name}: {level_name}".format( + profile_name=record.profile_id.name, + level_name=record.level_id.name, + ) + + @api.depends("manual_level_id", "computed_level_id") + def _compute_level_id(self): + for rec in self: + if rec.manual_level_id: + rec.level_id = rec.manual_level_id + else: + rec.level_id = rec.computed_level_id + + @api.depends("manual_level_id", "computed_level_id") + def _compute_flag(self): + for rec in self: + rec.flag = ( + rec.computed_level_id and rec.manual_level_id != rec.computed_level_id + ) + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if "manual_level_id" not in vals and "computed_level_id" in vals: + # at creation the manual level is set to the same value as the + # computed one + vals["manual_level_id"] = vals["computed_level_id"] + + if "profile_id" in vals: + profile = self.env["abc.classification.profile"].browse( + vals["profile_id"] + ) + if profile.auto_apply_computed_value and "computed_level_id" in vals: + vals["manual_level_id"] = vals["computed_level_id"] + return super().create(vals_list) + + def write(self, vals): + values = vals.copy() + if "profile_id" in values: + profile = self.env["abc.classification.profile"].browse( + values["profile_id"] + ) + else: + profile = self.mapped("profile_id") + + if profile.auto_apply_computed_value and "computed_level_id" in values: + values["manual_level_id"] = values["computed_level_id"] + return super().write(values) diff --git a/product_abc_classification/models/abc_classification_profile.py b/product_abc_classification/models/abc_classification_profile.py new file mode 100644 index 00000000000..b57b0a1e46d --- /dev/null +++ b/product_abc_classification/models/abc_classification_profile.py @@ -0,0 +1,96 @@ +# Copyright 2020 ForgeFlow +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from psycopg2.extensions import AsIs + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class AbcClassificationProfile(models.Model): + + _name = "abc.classification.profile" + _description = "Abc Classification Profile" + _rec_name = "name" + + name = fields.Char(required=True) + level_ids = fields.One2many( + comodel_name="abc.classification.level", inverse_name="profile_id" + ) + profile_type = fields.Selection( + selection=[], + string="Type of ABC classification", + index=True, + required=True, + ) + period = fields.Integer( + default=365, + string="Period on which to compute the classification (Days)", + required=True, + ) + + auto_apply_computed_value = fields.Boolean(default=False) + + _sql_constraints = [("name_uniq", "UNIQUE(name)", _("Profile name must be unique"))] + + @api.constrains("level_ids") + def _check_levels(self): + for profile in self: + percentages = profile.level_ids.mapped("percentage") + total = sum(percentages) + if profile.level_ids and total != 100.0: + raise ValidationError( + _("The sum of the percentages of the levels should be " "100.") + ) + if profile.level_ids and len({}.fromkeys(percentages)) != len(percentages): + raise ValidationError( + _("The percentages of the levels must be unique.") + ) + percentage_productss = profile.level_ids.mapped("percentage_products") + total = sum(percentage_productss) + if profile.level_ids and total != 100.0: + raise ValidationError( + _( + "The sum of the products percentages of the levels " + "should be 100." + ) + ) + + def _compute_abc_classification(self): + raise NotImplementedError() + + @api.model + def _cron_compute_abc_classification(self): + self.search([])._compute_abc_classification() + + def write(self, vals): + res = super(AbcClassificationProfile, self).write(vals) + if "auto_apply_computed_value" in vals and vals["auto_apply_computed_value"]: + self._auto_apply_computed_value_for_product_levels() + return res + + def _auto_apply_computed_value_for_product_levels(self): + level_ids = [] + for rec in self: + self.env.cr.execute( + """ + UPDATE %(table)s + SET manual_level_id = computed_level_id + WHERE profile_id = %(profile_id)s + RETURNING id + + """, + { + "table": AsIs(self.env["abc.classification.product.level"]._table), + "profile_id": rec.id, + }, + ) + level_ids.extend(r[0] for r in self.env.cr.fetchall()) + self.env["abc.classification.product.level"].invalidate_cache( + ["manual_level_id"], level_ids + ) + modified_levels = self.env["abc.classification.product.level"].browse(level_ids) + # mark field as modified and trigger recompute of dependent fields. + modified_levels.modified(["manual_level_id"]) + modified_levels._recompute_recordset() diff --git a/product_abc_classification/models/product_product.py b/product_abc_classification/models/product_product.py new file mode 100644 index 00000000000..80f1fc1513f --- /dev/null +++ b/product_abc_classification/models/product_product.py @@ -0,0 +1,21 @@ +# Copyright 2020 ForgeFlow +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ProductProduct(models.Model): + + _inherit = "product.product" + + abc_classification_product_level_ids = fields.One2many( + "abc.classification.product.level", index=True, inverse_name="product_id" + ) + abc_classification_profile_ids = fields.Many2many( + comodel_name="abc.classification.profile", + relation="abc_classification_profile_product_rel", + column1="product_id", + column2="profile_id", + index=True, + ) diff --git a/product_abc_classification/models/product_template.py b/product_abc_classification/models/product_template.py new file mode 100644 index 00000000000..48c01bbc47d --- /dev/null +++ b/product_abc_classification/models/product_template.py @@ -0,0 +1,70 @@ +# Copyright 2020 ForgeFlow +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + abc_classification_profile_ids = fields.Many2many( + "abc.classification.profile", + compute="_compute_abc_classification_profile_ids", + inverse="_inverse_abc_classification_profile_ids", + store=True, + ) + abc_classification_product_level_ids = fields.One2many( + "abc.classification.product.level", + compute="_compute_abc_classification_product_level_ids", + inverse="_inverse_abc_classification_product_level_ids", + inverse_name="product_tmpl_id", + store=True, + ) + + @api.depends( + "product_variant_ids", + "product_variant_ids.abc_classification_profile_ids", + ) + def _compute_abc_classification_profile_ids(self): + unique_variants = self.filtered( + lambda template: len(template.product_variant_ids) == 1 + ) + for template in unique_variants: + template.abc_classification_profile_ids = ( + template.product_variant_ids.abc_classification_profile_ids + ) + for template in self - unique_variants: + template.abc_classification_profile_ids = False + + @api.depends( + "product_variant_ids", + "product_variant_ids.abc_classification_product_level_ids", + ) + def _compute_abc_classification_product_level_ids(self): + unique_variants = self.filtered( + lambda template: len(template.product_variant_ids) == 1 + ) + for template in unique_variants: + variants = template.product_variant_ids + template.abc_classification_product_level_ids = ( + variants.abc_classification_product_level_ids + ) + for template in self - unique_variants: + template.abc_classification_product_level_ids = False + + def _inverse_abc_classification_profile_ids(self): + for template in self: + if len(template.product_variant_ids) == 1: + variants = template.product_variant_ids + variants.abc_classification_profile_ids = ( + template.abc_classification_profile_ids + ) + + def _inverse_abc_classification_product_level_ids(self): + for template in self: + if len(template.product_variant_ids) == 1: + variants = template.product_variant_ids + variants.abc_classification_product_level_ids = ( + template.abc_classification_product_level_ids + ) diff --git a/product_abc_classification/readme/CONTRIBUTORS.rst b/product_abc_classification/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..fe41e2ce43d --- /dev/null +++ b/product_abc_classification/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* Miquel Raïch +* Lindsay Marion +* Laurent Mignon +* Denis Roussel diff --git a/product_abc_classification/readme/DESCRIPTION.rst b/product_abc_classification/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..e8bc6d5b704 --- /dev/null +++ b/product_abc_classification/readme/DESCRIPTION.rst @@ -0,0 +1,11 @@ +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 portfolio and ensure they +prioritize managing them above those less valuable. + +Managers will create a profile with several levels (percentages) and then the +profiled products will automatically get a corresponding level using the +ABC classification. + +The addon *product_abc_classification_sale_stock* defines a computation profile +based on the number of sale order line delivered by product. diff --git a/product_abc_classification/readme/USAGE.rst b/product_abc_classification/readme/USAGE.rst new file mode 100644 index 00000000000..a66526e486c --- /dev/null +++ b/product_abc_classification/readme/USAGE.rst @@ -0,0 +1,11 @@ +To use this module, you need to: + +#. Go to Sales or Inventory menu, then to Configuration/Products/ABC Classification Profile +and create a profile with levels, knowing that the sum of all levels in the profile +should sum 100 and all the levels should be different. + +#. Later you should go to product categories or product variants, and assign them a profile. +Then the cron classification will proceed to assign to these products one of the profile's levels. + +NOTE: If you profile (or unprofile) a product category, then all its +child categories and products will be profiled (or unprofiled). diff --git a/product_abc_classification/security/ir.model.access.csv b/product_abc_classification/security/ir.model.access.csv new file mode 100644 index 00000000000..9283b353960 --- /dev/null +++ b/product_abc_classification/security/ir.model.access.csv @@ -0,0 +1,7 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_abc_classification_profile_user,abc.classification.profile.user,model_abc_classification_profile,base.group_user,1,0,0,0 +access_abc_classification_profile_manager,abc.classification.profile.manager,model_abc_classification_profile,stock.group_stock_manager,1,1,1,1 +access_abc_classification_level_user,abc.classification.level.user,model_abc_classification_level,base.group_user,1,0,0,0 +access_abc_classification_level_manager,abc.classification.level.manager,model_abc_classification_level,stock.group_stock_manager,1,1,1,1 +access_abc_classification_product_level_user,abc.classification.product.level.user,model_abc_classification_product_level,base.group_user,1,0,0,0 +access_abc_classification_product_level_manager,abc.classification.product.level.manager,model_abc_classification_product_level,stock.group_stock_manager,1,1,0,0 diff --git a/product_abc_classification/static/description/icon.png b/product_abc_classification/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/product_abc_classification/static/description/index.html b/product_abc_classification/static/description/index.html new file mode 100644 index 00000000000..b309d87953d --- /dev/null +++ b/product_abc_classification/static/description/index.html @@ -0,0 +1,443 @@ + + + + + + +Product Abc Classification + + + +
+

Product Abc Classification

+ + +

Beta License: AGPL-3 OCA/product-attribute Translate me on Weblate Try me on Runbot

+

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 portfolio and ensure they +prioritize managing them above those less valuable.

+

Managers will create a profile with several levels (percentages) and then the +profiled products will automatically get a corresponding level using the +ABC classification.

+

The addon product_abc_classification_sale_stock defines a computation profile +based on the number of sale order line delivered by product.

+

Table of contents

+ +
+

Usage

+

To use this module, you need to:

+

#. Go to Sales or Inventory menu, then to Configuration/Products/ABC Classification Profile +and create a profile with levels, knowing that the sum of all levels in the profile +should sum 100 and all the levels should be different.

+

#. Later you should go to product categories or product variants, and assign them a profile. +Then the cron classification will proceed to assign to these products one of the profile’s levels.

+

NOTE: If you profile (or unprofile) a product category, then all its +child categories and products will be profiled (or unprofiled).

+
+
+

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

+
    +
  • ACSONE SA/NV
  • +
  • ForgeFlow
  • +
+
+
+

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

+

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

+
+
+
+ + diff --git a/product_abc_classification/tests/__init__.py b/product_abc_classification/tests/__init__.py new file mode 100644 index 00000000000..8292c06ca32 --- /dev/null +++ b/product_abc_classification/tests/__init__.py @@ -0,0 +1,3 @@ +from . import test_abc_classification_product_level +from . import test_abc_classification_profile +from . import test_product diff --git a/product_abc_classification/tests/common.py b/product_abc_classification/tests/common.py new file mode 100644 index 00000000000..4900770fdce --- /dev/null +++ b/product_abc_classification/tests/common.py @@ -0,0 +1,123 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests.common import TransactionCase + + +class ABCClassificationCase(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + # add a fake profile_type + cls.ABCClassificationProfile = cls.env["abc.classification.profile"] + cls.ABCClassificationProfile._fields["profile_type"].selection = [ + ("test_type", "Test Type") + ] + cls.classification_profile = cls.ABCClassificationProfile.create( + {"name": "Profile test", "profile_type": "test_type"} + ) + + +class ABCClassificationLevelCase(ABCClassificationCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.classification_profile.write( + { + "level_ids": [ + ( + 0, + 0, + { + "percentage": 60, + "percentage_products": 40, + "name": "a", + }, + ), + ( + 0, + 0, + { + "percentage": 40, + "percentage_products": 60, + "name": "b", + }, + ), + ] + } + ) + + levels = cls.classification_profile.level_ids + cls.classification_level_a = levels.filtered(lambda l: l.name == "a") + cls.classification_level_b = levels.filtered(lambda l: l.name == "b") + cls.classification_profile_bis = cls.ABCClassificationProfile.create( + { + "name": "Profile test bis", + "profile_type": "test_type", + "level_ids": [ + ( + 0, + 0, + { + "percentage": 80, + "percentage_products": 40, + "name": "a", + }, + ), + ( + 0, + 0, + { + "percentage": 20, + "percentage_products": 60, + "name": "b", + }, + ), + ], + } + ) + levels = cls.classification_profile_bis.level_ids + cls.classification_level_bis_a = levels.filtered(lambda l: l.name == "a") + + cls.classification_level_bis_b = levels.filtered(lambda l: l.name == "b") + # create a template with one variant adn declare attributes to create + # an other variant on demand + cls.size_attr = cls.env["product.attribute"].create( + { + "name": "Size", + "create_variant": "no_variant", + "value_ids": [(0, 0, {"name": "S"}), (0, 0, {"name": "M"})], + } + ) + cls.size_attr_value_s = cls.size_attr.value_ids[0] + cls.size_attr_value_m = cls.size_attr.value_ids[1] + cls.uom_unit = cls.env.ref("uom.product_uom_unit") + cls.product_template = cls.env["product.template"].create( + { + "name": "Test sized", + "uom_id": cls.uom_unit.id, + "uom_po_id": cls.uom_unit.id, + "attribute_line_ids": [ + ( + 0, + 0, + { + "attribute_id": cls.size_attr.id, + "value_ids": [(6, 0, cls.size_attr.value_ids.ids)], + }, + ) + ], + } + ) + cls.product_product = cls.product_template.product_variant_ids + cls.ProductLevel = cls.env["abc.classification.product.level"] + + @classmethod + def _create_variant(cls, size_value): + return cls.env["product.product"].create( + { + "product_tmpl_id": cls.product_template.id, + "product_template_attribute_value_ids": [(6, 0, size_value.ids)], + } + ) diff --git a/product_abc_classification/tests/test_abc_classification_product_level.py b/product_abc_classification/tests/test_abc_classification_product_level.py new file mode 100644 index 00000000000..9b2db6df872 --- /dev/null +++ b/product_abc_classification/tests/test_abc_classification_product_level.py @@ -0,0 +1,363 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from psycopg2 import IntegrityError + +from odoo.exceptions import ValidationError + +from .common import ABCClassificationLevelCase + + +class TestABCClassificationProductLevel(ABCClassificationLevelCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.product_1 = cls.env["product.product"].create( + { + "name": "Test 1", + "uom_id": cls.uom_unit.id, + "uom_po_id": cls.uom_unit.id, + } + ) + cls.product_level = cls.ProductLevel.create( + { + "product_id": cls.product_product.id, + "computed_level_id": cls.classification_level_a.id, + "profile_id": cls.classification_profile.id, + } + ) + + @classmethod + def _create_product_levels(cls): + product_2 = cls.env["product.product"].create( + { + "name": "Test 2", + "uom_id": cls.uom_unit.id, + "uom_po_id": cls.uom_unit.id, + } + ) + + product_3 = cls.env["product.product"].create( + { + "name": "Test 3", + "uom_id": cls.uom_unit.id, + "uom_po_id": cls.uom_unit.id, + } + ) + cls.ProductLevel.create( + { + "product_id": product_2.id, + "manual_level_id": cls.classification_level_b.id, + "computed_level_id": cls.classification_level_a.id, + "profile_id": cls.classification_profile.id, + } + ) + cls.ProductLevel.create( + { + "product_id": product_3.id, + "manual_level_id": cls.classification_level_b.id, + "computed_level_id": cls.classification_level_a.id, + "profile_id": cls.classification_profile.id, + } + ) + + def test_00(self): + """ + Test case: + Create a classification product level with only a computed_level_id + Expected result: + A instance is created with: + * the manual_level_id and level_id set + * flag is False since manual and computd are the same + + """ + level = self.ProductLevel.create( + { + "product_id": self.product_1.id, + "computed_level_id": self.classification_level_a.id, + "profile_id": self.classification_profile.id, + } + ) + self.assertEqual(level.manual_level_id, self.classification_level_a) + self.assertEqual(level.level_id, self.classification_level_a) + self.assertFalse(level.flag) + + def test_01(self): + """ + Test case: + Create product level with only a manual level + + A creation if a product level is created without computed value + the computed value is never taken into account + Expected result: + A new level is create with: + * computed_level_id = False + * level_id = manual_level_id + * flag = False + """ + level = self.ProductLevel.create( + { + "product_id": self.product_1.id, + "manual_level_id": self.classification_level_a.id, + "profile_id": self.classification_profile.id, + } + ) + self.assertFalse(level.computed_level_id) + self.assertEqual(level.manual_level_id, self.classification_level_a) + self.assertEqual(level.level_id, self.classification_level_a) + self.assertFalse(level.flag) + + def test_02(self): + """ + Data: + An existing classification level with computed = manual + Test case: + 1. Change manual_level_id to an other value than the computed one + 2. Reset manual_level_id to the computed one + Expected result: + 1. level_id === manual =! computed and flag is true + 2 level_id == manual == computed and flag is true + ValidationError + """ + self.assertFalse(self.product_level.flag) + self.assertEqual( + self.product_level.manual_level_id, + self.product_level.computed_level_id, + ) + self.assertEqual( + self.product_level.computed_level_id, self.classification_level_a + ) + self.assertEqual(self.product_level.level_id, self.classification_level_a) + # 1 + self.product_level.manual_level_id = self.classification_level_b + self.assertEqual(self.product_level.level_id, self.classification_level_b) + self.assertTrue(self.product_level.flag) + # 2 + self.product_level.manual_level_id = self.product_level.computed_level_id + self.assertEqual(self.product_level.level_id, self.classification_level_a) + self.assertFalse(self.product_level.flag) + + def test_03(self): + """ + Data: + An existing product level + Test case: + Create a new product level for the same product and the same profile + Expected result: + IntegrityError (level name must be unique by profile and product) + """ + with self.assertRaises(IntegrityError): + self.ProductLevel.create( + { + "product_id": self.product_product.id, + "computed_level_id": self.classification_level_a.id, + "profile_id": self.classification_profile.id, + } + ) + + def test_04(self): + """ + Data: + An existing product level + Test case: + 1. Link a manual level from an other profile + 2. Link a computed level from an other profile + Expected result: + 1. and 2. Validation error (All the levels must share the same + profile as the one on the product level) + """ + with self.assertRaises(ValidationError), self.env.cr.savepoint(): + self.product_level.write( + { + "manual_level_id": self.classification_level_b.id, + "computed_level_id": self.classification_level_bis_a.id, + } + ) + with self.assertRaises(ValidationError), self.env.cr.savepoint(): + self.product_level.write( + { + "manual_level_id": self.classification_level_bis_a.id, + "computed_level_id": self.classification_level_a.id, + } + ) + self.product_level.write( + { + "manual_level_id": self.classification_level_bis_a.id, + "computed_level_id": self.classification_level_bis_a.id, + "profile_id": self.classification_profile_bis.id, + } + ) + + def test_05(self): + """ + Test case: + Create a product level without computed nor manual level + Expected result: + Validation error (at least a value for one of these fields is + expected) + """ + with self.assertRaises(ValidationError): + self.ProductLevel.create( + { + "product_id": self.product_1.id, + "profile_id": self.classification_profile.id, + } + ) + + def test_06_update_product_level_with_auto_compute(self): + self.classification_profile_bis.auto_apply_computed_value = True + self.product_level.write( + { + "computed_level_id": self.classification_level_bis_a.id, + "profile_id": self.classification_profile_bis.id, + } + ) + + self.assertEqual( + self.product_level.manual_level_id, + self.product_level.computed_level_id, + ) + self.assertEqual( + self.product_level.computed_level_id, self.classification_level_bis_a + ) + self.assertEqual(self.product_level.level_id, self.classification_level_bis_a) + + self.product_level.write( + { + "computed_level_id": self.classification_level_bis_b.id, + } + ) + self.assertEqual( + self.product_level.manual_level_id, + self.product_level.computed_level_id, + ) + self.assertEqual( + self.product_level.computed_level_id, self.classification_level_bis_b + ) + self.assertEqual(self.product_level.level_id, self.classification_level_bis_b) + + def test_07_update_product_level_without_auto_compute(self): + self.classification_profile.auto_apply_computed_value = False + self.product_level.write( + { + "manual_level_id": self.classification_level_b.id, + "computed_level_id": self.classification_level_a.id, + "profile_id": self.classification_profile.id, + } + ) + + self.assertNotEqual( + self.product_level.manual_level_id, + self.product_level.computed_level_id, + ) + self.assertEqual( + self.product_level.computed_level_id, self.classification_level_a + ) + self.assertEqual( + self.product_level.manual_level_id, self.classification_level_b + ) + self.assertEqual(self.product_level.level_id, self.classification_level_b) + + self.product_level.write( + { + "manual_level_id": self.classification_level_a.id, + "computed_level_id": self.classification_level_b.id, + } + ) + + self.assertNotEqual( + self.product_level.manual_level_id, + self.product_level.computed_level_id, + ) + self.assertEqual( + self.product_level.computed_level_id, self.classification_level_b + ) + self.assertEqual( + self.product_level.manual_level_id, self.classification_level_a + ) + self.assertEqual(self.product_level.level_id, self.classification_level_a) + + def test_08_update_recordset_with__autocompute(self): + self._create_product_levels() + self.classification_profile.auto_apply_computed_value = True + + levels = self.ProductLevel.search( + [("profile_id", "=", self.classification_profile.id)] + ) + levels.write( + { + "manual_level_id": self.classification_level_a.id, + "computed_level_id": self.classification_level_b.id, + } + ) + + for level in levels: + self.assertEqual(level.manual_level_id, level.computed_level_id) + self.assertEqual(level.manual_level_id, self.classification_level_b) + self.assertEqual(level.computed_level_id, self.classification_level_b) + self.assertEqual(level.level_id, self.classification_level_b) + + def test_09_update_recordset_and_change_profile(self): + self._create_product_levels() + self.classification_profile_bis.auto_apply_computed_value = True + + levels = self.ProductLevel.search( + [("profile_id", "=", self.classification_profile.id)] + ) + levels.write( + { + "computed_level_id": self.classification_level_bis_a.id, + "profile_id": self.classification_profile_bis.id, + } + ) + + for level in levels: + self.assertEqual(level.manual_level_id, level.computed_level_id) + self.assertEqual(level.manual_level_id, self.classification_level_bis_a) + self.assertEqual(level.computed_level_id, self.classification_level_bis_a) + self.assertEqual(level.level_id, self.classification_level_bis_a) + + def test_10_create_product_level_for_profile_auto_assign(self): + self.classification_profile.auto_apply_computed_value = True + level = self.ProductLevel.create( + { + "product_id": self.product_1.id, + "manual_level_id": self.classification_level_b.id, + "computed_level_id": self.classification_level_a.id, + "profile_id": self.classification_profile.id, + } + ) + self.assertEqual(level.manual_level_id, level.computed_level_id) + self.assertEqual(level.manual_level_id, self.classification_level_a) + self.assertEqual(level.computed_level_id, self.classification_level_a) + self.assertEqual(level.level_id, self.classification_level_a) + + def test_11_auto_apply_computed_level(self): + self._create_product_levels() + + levels = self.ProductLevel.search( + [("profile_id", "=", self.classification_profile.id)] + ) + level0 = levels[0] + level1 = levels[1] + level2 = levels[2] + self.assertEqual(level0.manual_level_id, level0.computed_level_id) + self.assertEqual(level0.manual_level_id, self.classification_level_a) + self.assertEqual(level0.computed_level_id, self.classification_level_a) + self.assertEqual(level0.level_id, self.classification_level_a) + + self.assertNotEqual(level1.manual_level_id, level1.computed_level_id) + self.assertEqual(level1.manual_level_id, self.classification_level_b) + self.assertEqual(level1.computed_level_id, self.classification_level_a) + self.assertEqual(level1.level_id, self.classification_level_b) + + self.assertNotEqual(level2.manual_level_id, level2.computed_level_id) + self.assertEqual(level2.manual_level_id, self.classification_level_b) + self.assertEqual(level2.computed_level_id, self.classification_level_a) + self.assertEqual(level2.level_id, self.classification_level_b) + + self.classification_profile.auto_apply_computed_value = True + for level in levels: + self.assertEqual(level.manual_level_id, self.classification_level_a) + self.assertEqual(level.computed_level_id, self.classification_level_a) + self.assertEqual(level.level_id, self.classification_level_a) diff --git a/product_abc_classification/tests/test_abc_classification_profile.py b/product_abc_classification/tests/test_abc_classification_profile.py new file mode 100644 index 00000000000..51691913cf2 --- /dev/null +++ b/product_abc_classification/tests/test_abc_classification_profile.py @@ -0,0 +1,300 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from psycopg2 import IntegrityError + +from odoo.exceptions import ValidationError + +from .common import ABCClassificationCase + + +class TestABCClassificationProfile(ABCClassificationCase): + def test_00(self): + """ + Data: + A test profile + Test case: + Assign levels for a total of 100% + Expected result: + OK + """ + self.classification_profile.write( + { + "level_ids": [ + ( + 0, + 0, + { + "percentage": 60, + "percentage_products": 40, + "name": "A", + }, + ), + ( + 0, + 0, + { + "percentage": 40, + "percentage_products": 60, + "name": "B", + }, + ), + ] + } + ) + self.assertEqual(len(self.classification_profile.level_ids), 2) + + def test_01(self): + """ + Data: + A test profile + Test case: + Assign levels for a total < 100% + Expected result: + ValidationError + """ + with self.assertRaises(ValidationError): + self.classification_profile.write( + { + "level_ids": [ + ( + 0, + 0, + { + "percentage": 60, + "percentage_products": 40, + "name": "A", + }, + ), + ( + 0, + 0, + { + "percentage": 30, + "percentage_products": 60, + "name": "B", + }, + ), + ] + } + ) + + def test_02(self): + """ + Data: + A test profile + Test case: + Assign levels for a total > 100% + Expected result: + ValidationError + """ + with self.assertRaises(ValidationError): + self.classification_profile.write( + { + "level_ids": [ + ( + 0, + 0, + { + "percentage": 60, + "percentage_products": 40, + "name": "A", + }, + ), + ( + 0, + 0, + { + "percentage": 50, + "percentage_products": 60, + "name": "B", + }, + ), + ] + } + ) + + def test_03(self): + """ + Data: + A test profile + Test case: + Assign levels for a total = 100% but with same percentage + Expected result: + ValidationError + """ + with self.assertRaises(ValidationError): + self.classification_profile.write( + { + "level_ids": [ + ( + 0, + 0, + { + "percentage": 50, + "percentage_products": 40, + "name": "A", + }, + ), + ( + 0, + 0, + { + "percentage": 50, + "percentage_products": 60, + "name": "B", + }, + ), + ] + } + ) + + def test_04(self): + """ + Data: + A test profile + Test case: + Assign levels for a total = 100% but with one level with negative + percentage and one level exceeding 100% + Expected result: + ValidationError + """ + with self.assertRaises(ValidationError): + self.classification_profile.write( + { + "level_ids": [ + ( + 0, + 0, + { + "percentage": 150, + "percentage_products": 40, + "name": "A", + }, + ), + ( + 0, + 0, + { + "percentage": -50, + "percentage_products": 60, + "name": "B", + }, + ), + ] + } + ) + + def test_05(self): + """ + Data: + A test profile + Test case: + Assign levels for a total = 100% but with same name + Expected result: + IntegrityError (level name must be unique by profile) + """ + with self.assertRaises(IntegrityError): + self.classification_profile.write( + { + "level_ids": [ + ( + 0, + 0, + { + "percentage": 60, + "percentage_products": 40, + "name": "A", + }, + ), + ( + 0, + 0, + { + "percentage": 40, + "percentage_products": 60, + "name": "A", + }, + ), + ] + } + ) + + def test_06(self): + """ + Data: + A test profile with 2 levels A and B + Test case: + Create a new profile with the same level name + Expected result: + Profile created without error since the level name is unique by + profile + """ + self.classification_profile.write( + { + "level_ids": [ + ( + 0, + 0, + { + "percentage": 60, + "percentage_products": 40, + "name": "A", + }, + ), + ( + 0, + 0, + { + "percentage": 40, + "percentage_products": 60, + "name": "B", + }, + ), + ] + } + ) + new_profile = self.ABCClassificationProfile.create( + { + "name": "New Profile test", + "profile_type": "test_type", + "level_ids": [ + ( + 0, + 0, + { + "percentage": 60, + "percentage_products": 40, + "name": "A", + }, + ), + ( + 0, + 0, + { + "percentage": 40, + "percentage_products": 60, + "name": "B", + }, + ), + ], + } + ) + self.assertTrue(new_profile) + + def test_07(self): + """ + Data: + A test profile + Test case: + Create a new profile with the same name + Expected result: + IntegrityError (profile name must be unique by profile) + """ + with self.assertRaises(IntegrityError): + self.ABCClassificationProfile.create( + { + "name": self.classification_profile.name, + "profile_type": "test_type", + } + ) diff --git a/product_abc_classification/tests/test_product.py b/product_abc_classification/tests/test_product.py new file mode 100644 index 00000000000..923ae1eefe9 --- /dev/null +++ b/product_abc_classification/tests/test_product.py @@ -0,0 +1,109 @@ +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from .common import ABCClassificationLevelCase + + +class TestProduct(ABCClassificationLevelCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + def test_00(self): + """ + Data: + A product template with one variant. + Test Case: + 1. Associate a classification profile to the template + 2. Unset the classifiation profile + Expected: + 1. The classification profile is also associated to the variant + 2. The classification profile no more associated to the variant + """ + self.assertFalse(self.product_template.abc_classification_profile_ids) + self.assertFalse(self.product_product.abc_classification_profile_ids) + # 1 + self.product_template.abc_classification_profile_ids = ( + self.classification_profile + ) + self.assertEqual( + self.product_product.abc_classification_profile_ids, + self.classification_profile, + ) + # 2 + self.product_template.abc_classification_profile_ids = False + self.assertFalse(self.product_product.abc_classification_profile_ids) + + def test_01(self): + """ + Data: + A product template with two variants (without profiles). + Test Case: + 1. Associate a classification profile to the template + Expected: + The classification profile is not associated to the variant + """ + self._create_variant(self.size_attr_value_m) + variants = self.product_template.product_variant_ids + self.assertEqual(len(variants), 2) + self.assertFalse(variants.mapped("abc_classification_profile_ids")) + self.product_template.abc_classification_profile_ids = ( + self.classification_profile + ) + self.assertFalse(variants.mapped("abc_classification_profile_ids")) + + def test_02(self): + """ + Data: + A product template with one variant + Test Case: + 1 Associate a product level to the variant + 2 unlink the level + Expected result: + 1 The product level is also associated to the template + 2 No more level associated to the template + """ + product_level = self.ProductLevel.create( + { + "product_id": self.product_product.id, + "computed_level_id": self.classification_level_a.id, + "profile_id": self.classification_profile.id, + } + ) + self.assertEqual( + self.product_product.abc_classification_product_level_ids, + product_level, + ) + self.assertEqual( + self.product_template.abc_classification_product_level_ids, + product_level, + ) + product_level.unlink() + + self.assertFalse(self.product_product.abc_classification_product_level_ids) + self.assertFalse(self.product_template.abc_classification_product_level_ids) + + def test_03(self): + """ + Data: + A product template with two variants + Test Case: + Associate a product level to one variant + Expected result: + The product level is not associated to the template + """ + new_variant = self._create_variant(self.size_attr_value_m) + variants = self.product_template.product_variant_ids + self.assertEqual(len(variants), 2) + product_level = self.ProductLevel.create( + { + "product_id": new_variant.id, + "computed_level_id": self.classification_level_a.id, + "profile_id": self.classification_profile.id, + } + ) + self.assertEqual( + new_variant.abc_classification_product_level_ids, + product_level, + ) + self.assertFalse(self.product_template.abc_classification_product_level_ids) diff --git a/product_abc_classification/views/abc_classification_product_level.xml b/product_abc_classification/views/abc_classification_product_level.xml new file mode 100644 index 00000000000..71ac10979bd --- /dev/null +++ b/product_abc_classification/views/abc_classification_product_level.xml @@ -0,0 +1,101 @@ + + + + + abc.classification.product.level.form (in product_abc_classification) + abc.classification.product.level + +
+ + + + + + + + + + + + + + + +
+ + +
+
+
+
+ + abc.classification.product.level.tree (in product_abc_classification) + abc.classification.product.level + + + + + + + + + + + + abc.classification.product.level.search (in product_abc_classification) + abc.classification.product.level + + + + + + + + + + + + + + + + Products ABC Classification + abc.classification.product.level + tree,form + {'search_default_group_by_level': 1} + + +
diff --git a/product_abc_classification/views/abc_classification_profile.xml b/product_abc_classification/views/abc_classification_profile.xml new file mode 100644 index 00000000000..f7eaf1f93da --- /dev/null +++ b/product_abc_classification/views/abc_classification_profile.xml @@ -0,0 +1,54 @@ + + + + + abc.classification.profile.form (in product_abc_classification) + abc.classification.profile + +
+ + + + + + + + + + + + + + + + + +
+
+
+ + abc.classification.profile.tree (in product_abc_classification) + abc.classification.profile + + + + + + + + ABC Classification profiles + abc.classification.profile + tree,form + + +
diff --git a/product_abc_classification/views/product_product.xml b/product_abc_classification/views/product_product.xml new file mode 100644 index 00000000000..ef810e32598 --- /dev/null +++ b/product_abc_classification/views/product_product.xml @@ -0,0 +1,19 @@ + + + + + product.product.form (ABC Classification) + product.product + + + + {'default_product_id': active_id, 'default_profile_id': abc_classification_profile_ids and abc_classification_profile_ids[0] and abc_classification_profile_ids[0][2] and abc_classification_profile_ids[0][2][0] or False} + {'read_only': False} + [('product_id', '=', active_id)] + + + + diff --git a/product_abc_classification/views/product_template.xml b/product_abc_classification/views/product_template.xml new file mode 100644 index 00000000000..1127c274e5f --- /dev/null +++ b/product_abc_classification/views/product_template.xml @@ -0,0 +1,32 @@ + + + + + product.template.form (ABC Classification) + product.template + + + + + + + + + + + + + + + From 7242b45d49321c5cf298d8c9a7e2bb726b61dc76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miquel=20Ra=C3=AFch?= Date: Wed, 2 Feb 2022 17:34:19 +0100 Subject: [PATCH 04/22] [IMP] product_abc_classification: add product smart button in profile --- .../models/abc_classification_profile.py | 37 +++++++++++++++++++ .../views/abc_classification_profile.xml | 10 +++++ 2 files changed, 47 insertions(+) diff --git a/product_abc_classification/models/abc_classification_profile.py b/product_abc_classification/models/abc_classification_profile.py index b57b0a1e46d..6411834d0ee 100644 --- a/product_abc_classification/models/abc_classification_profile.py +++ b/product_abc_classification/models/abc_classification_profile.py @@ -30,6 +30,15 @@ class AbcClassificationProfile(models.Model): required=True, ) + product_variant_ids = fields.Many2many( + comodel_name="product.product", + relation="abc_classification_profile_product_rel", + column1="profile_id", + column2="product_id", + index=True, + ) + product_count = fields.Integer(compute="_compute_product_count", readonly=True) + auto_apply_computed_value = fields.Boolean(default=False) _sql_constraints = [("name_uniq", "UNIQUE(name)", _("Profile name must be unique"))] @@ -60,6 +69,34 @@ def _check_levels(self): def _compute_abc_classification(self): raise NotImplementedError() + @api.depends("product_variant_ids") + def _compute_product_count(self): + for profile in self: + profile.product_count = len(profile.product_variant_ids) + + def action_view_products(self): + products = self.mapped("product_variant_ids") + action = self.env["ir.actions.act_window"].for_xml_id( + "product", "product_variant_action" + ) + del action["context"] + if len(products) > 1: + action["domain"] = [("id", "in", products.ids)] + elif len(products) == 1: + form_view = [ + (self.env.ref("product.product_variant_easy_edit_view").id, "form") + ] + if "views" in action: + action["views"] = form_view + [ + (state, view) for state, view in action["views"] if view != "form" + ] + else: + action["views"] = form_view + action["res_id"] = products.id + else: + action = {"type": "ir.actions.act_window_close"} + return action + @api.model def _cron_compute_abc_classification(self): self.search([])._compute_abc_classification() diff --git a/product_abc_classification/views/abc_classification_profile.xml b/product_abc_classification/views/abc_classification_profile.xml index f7eaf1f93da..65830d78c4f 100644 --- a/product_abc_classification/views/abc_classification_profile.xml +++ b/product_abc_classification/views/abc_classification_profile.xml @@ -10,6 +10,16 @@
+
+ +
From b07283fc7037795658a49b8d26e4ab3586e54414 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Tue, 15 Nov 2022 08:13:45 +0100 Subject: [PATCH 05/22] [FIX] product_abc_classification: Remove not working context attributes --- product_abc_classification/views/product_product.xml | 2 +- product_abc_classification/views/product_template.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/product_abc_classification/views/product_product.xml b/product_abc_classification/views/product_product.xml index ef810e32598..b6b19dc26ca 100644 --- a/product_abc_classification/views/product_product.xml +++ b/product_abc_classification/views/product_product.xml @@ -10,7 +10,7 @@ {'default_product_id': active_id, 'default_profile_id': abc_classification_profile_ids and abc_classification_profile_ids[0] and abc_classification_profile_ids[0][2] and abc_classification_profile_ids[0][2][0] or False} + >{'default_product_id': active_id, 'default_profile_id': abc_classification_profile_ids and abc_classification_profile_ids[0] or False} {'read_only': False} [('product_id', '=', active_id)] diff --git a/product_abc_classification/views/product_template.xml b/product_abc_classification/views/product_template.xml index 1127c274e5f..2f2873568fc 100644 --- a/product_abc_classification/views/product_template.xml +++ b/product_abc_classification/views/product_template.xml @@ -18,7 +18,7 @@ From d7e061cdd3677bcbed504546940c07aa1567c152 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Thu, 17 Nov 2022 14:23:31 +0100 Subject: [PATCH 06/22] [FIX] product_abc_classification: Use the good _for_xml_id() and add tests --- .../models/abc_classification_profile.py | 4 +-- .../tests/test_product.py | 30 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/product_abc_classification/models/abc_classification_profile.py b/product_abc_classification/models/abc_classification_profile.py index 6411834d0ee..c71f4b4bb63 100644 --- a/product_abc_classification/models/abc_classification_profile.py +++ b/product_abc_classification/models/abc_classification_profile.py @@ -76,8 +76,8 @@ def _compute_product_count(self): def action_view_products(self): products = self.mapped("product_variant_ids") - action = self.env["ir.actions.act_window"].for_xml_id( - "product", "product_variant_action" + action = self.env["ir.actions.act_window"]._for_xml_id( + "product.product_variant_action" ) del action["context"] if len(products) > 1: diff --git a/product_abc_classification/tests/test_product.py b/product_abc_classification/tests/test_product.py index 923ae1eefe9..add08143a4a 100644 --- a/product_abc_classification/tests/test_product.py +++ b/product_abc_classification/tests/test_product.py @@ -107,3 +107,33 @@ def test_03(self): product_level, ) self.assertFalse(self.product_template.abc_classification_product_level_ids) + + def test_04(self): + """ + Data: + A product template + Test case: + Check if resource id in action is the product variant one + """ + self.product_template.abc_classification_profile_ids = ( + self.classification_profile + ) + action = self.classification_profile.action_view_products() + self.assertEqual(action["res_id"], self.product_template.product_variant_ids.id) + + def test_05(self): + """ + Data: + A product template with two variants + Test case: + Check if doamin in action is the product variants ids + """ + self._create_variant(self.size_attr_value_m) + self.product_template.product_variant_ids.abc_classification_profile_ids = ( + self.classification_profile + ) + action = self.classification_profile.action_view_products() + self.assertEqual( + action["domain"], + [("id", "in", self.product_template.product_variant_ids.ids)], + ) From e777c86b9908bb329a72d827a5621772a5d614e2 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Tue, 22 Nov 2022 10:47:00 +0100 Subject: [PATCH 07/22] [FIX] product_abc_classification: Adapt write() for a multi recordset --- .../abc_classification_product_level.py | 40 ++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/product_abc_classification/models/abc_classification_product_level.py b/product_abc_classification/models/abc_classification_product_level.py index 1ad9cf6c184..7def032e335 100644 --- a/product_abc_classification/models/abc_classification_product_level.py +++ b/product_abc_classification/models/abc_classification_product_level.py @@ -146,14 +146,36 @@ def create(self, vals_list): return super().create(vals_list) def write(self, vals): + """ + We apply the manual level to the product level if + computed level is modified and only for profiles with + auto_apply_computed_value = =True + """ values = vals.copy() - if "profile_id" in values: - profile = self.env["abc.classification.profile"].browse( - values["profile_id"] + new_self = self + if "computed_level_id" in values: + profile_obj = self.env["abc.classification.profile"] + target_profile_id = ( + profile_obj.browse(values["profile_id"]).filtered( + "auto_apply_computed_value" + ) + if "profile_id" in values + else profile_obj.browse() ) - else: - profile = self.mapped("profile_id") - - if profile.auto_apply_computed_value and "computed_level_id" in values: - values["manual_level_id"] = values["computed_level_id"] - return super().write(values) + if target_profile_id: + # If the profile of levels should be changed at the same time + # and has auto_apply_computed_value True + # So, we can apply change to the whole recordset + values["manual_level_id"] = values["computed_level_id"] + else: + # If profile is not modified, filter levels per profile + # if it has auto_apply_computed_value True and modify only + # those ones + auto_applied_profiles_levels = self.filtered( + lambda l: l.profile_id.auto_apply_computed_value + ) + new_self = self - auto_applied_profiles_levels + super( + AbcClassificationProductLevel, auto_applied_profiles_levels + ).write(dict(values, manual_level_id=values["computed_level_id"])) + return super(AbcClassificationProductLevel, new_self).write(values) From 5f148ff1136c7b4298470866af1f2c8c5615ec33 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Wed, 15 Feb 2023 12:51:41 +0100 Subject: [PATCH 08/22] [IMP] product_abc_classification: Improve profile view + help --- .../models/abc_classification_profile.py | 6 +- .../views/abc_classification_profile.xml | 111 +++++++++++------- 2 files changed, 71 insertions(+), 46 deletions(-) diff --git a/product_abc_classification/models/abc_classification_profile.py b/product_abc_classification/models/abc_classification_profile.py index c71f4b4bb63..dc7fd29f3ad 100644 --- a/product_abc_classification/models/abc_classification_profile.py +++ b/product_abc_classification/models/abc_classification_profile.py @@ -39,7 +39,11 @@ class AbcClassificationProfile(models.Model): ) product_count = fields.Integer(compute="_compute_product_count", readonly=True) - auto_apply_computed_value = fields.Boolean(default=False) + auto_apply_computed_value = fields.Boolean( + default=False, + help="Check this if you want to apply the computed level on each product that has this " + "profile.", + ) _sql_constraints = [("name_uniq", "UNIQUE(name)", _("Profile name must be unique"))] diff --git a/product_abc_classification/views/abc_classification_profile.xml b/product_abc_classification/views/abc_classification_profile.xml index 65830d78c4f..89b078b40da 100644 --- a/product_abc_classification/views/abc_classification_profile.xml +++ b/product_abc_classification/views/abc_classification_profile.xml @@ -2,60 +2,81 @@ - - + abc.classification.profile.form (in product_abc_classification) - abc.classification.profile - - - -
- -
- - - - - - - - - - - - - - - -
- -
-
- - + + +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + abc.classification.profile.tree (in product_abc_classification) - abc.classification.profile - - - - - - - - ABC Classification profiles - abc.classification.profile - tree,form - - abc.classification.profile + + + + + + + + ABC Classification profiles + abc.classification.profile + tree,form + + Date: Wed, 5 Apr 2023 16:04:33 +0200 Subject: [PATCH 09/22] [FIX] product_abc_classification: Translated terms into views --- product_abc_classification/i18n/fr.po | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/product_abc_classification/i18n/fr.po b/product_abc_classification/i18n/fr.po index 47bc99abc3f..dffcbb0a26c 100644 --- a/product_abc_classification/i18n/fr.po +++ b/product_abc_classification/i18n/fr.po @@ -18,7 +18,7 @@ msgstr "" #. module: product_abc_classification #: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_percentage msgid "% Indicator" -msgstr "% KPI +msgstr "% KPI" #. module: product_abc_classification #: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_percentage_products @@ -26,12 +26,12 @@ msgid "% Products" msgstr "% Articles" #. module: product_abc_classification -#: model:ir.ui.view,arch_db:product_abc_classification.product_template_form_view +#: model_terms:ir.ui.view,arch_db:product_abc_classification.product_template_form_view msgid "ABC Classification" msgstr "Classification ABC" #. module: product_abc_classification -#: model:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_form_view +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_form_view msgid "ABC Classification Product Level" msgstr "Niveau de classification ABC des articles" @@ -42,12 +42,12 @@ msgid "ABC Classification profiles" msgstr "Profils de classification ABC" #. module: product_abc_classification -#: model:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_form_view +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_form_view msgid "ABC Profile" msgstr "Profil ABC" #. module: product_abc_classification -#: model:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_tree_view +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_tree_view msgid "ABC Profiles" msgstr "Profils ABC" @@ -62,7 +62,7 @@ msgid "Abc Classification Profile" msgstr "Profil de classification ABC" #. module: product_abc_classification -#: model:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view msgid "Abc classification" msgstr "Classification ABC" @@ -103,7 +103,7 @@ msgid "Classification level is mandatory" msgstr "La classe / niveau est obligatoire" #. module: product_abc_classification -#: model:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view msgid "Classification not in sync with computed" msgstr "Classes ABC manuelle et calculée divergentes" @@ -113,7 +113,7 @@ msgid "Computed classification level" msgstr "Classe calculée" #. module: product_abc_classification -#: model:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_form_view +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_form_view msgid "Computed level differs from the specified level" msgstr "La class calculée diverge de la valeur spécifiée" @@ -145,7 +145,7 @@ msgid "Display Name" msgstr "Nom affiché" #. module: product_abc_classification -#: model:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view msgid "Group By" msgstr "Grouper par" @@ -183,7 +183,7 @@ msgid "Last Updated on" msgstr "Dernière mise à jour le" #. module: product_abc_classification -#: model:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view msgid "Level" msgstr "Niveau" @@ -252,7 +252,7 @@ msgstr "Classification ABC des articles" #. module: product_abc_classification #: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_profile_id -#: model:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view msgid "Profile" msgstr "Profil" From bf6e878ad9a379c5d73da0e787074c1d952a82c9 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Mon, 18 Sep 2023 11:43:52 +0200 Subject: [PATCH 10/22] [FIX] product_abc_classification: Fix tests --- product_abc_classification/README.rst | 23 +- product_abc_classification/i18n/fr.po | 356 ++++++++++---- .../i18n/product_abc_classification.pot | 456 ++++++++++++++++++ .../static/description/index.html | 42 +- product_abc_classification/tests/common.py | 7 +- .../tests/test_abc_classification_profile.py | 3 + 6 files changed, 761 insertions(+), 126 deletions(-) create mode 100644 product_abc_classification/i18n/product_abc_classification.pot diff --git a/product_abc_classification/README.rst b/product_abc_classification/README.rst index bbed589ebf5..a5ba9334ace 100644 --- a/product_abc_classification/README.rst +++ b/product_abc_classification/README.rst @@ -2,10 +2,13 @@ Product Abc Classification ========================== -.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:8a366c55660284e3ef7f90bca5d9f06cd061ff819a5524aec87e67efb6fcf7a0 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status @@ -14,16 +17,16 @@ Product Abc Classification :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 - :target: https://github.com/OCA/product-attribute/tree/16.0/product_abc_classification_base + :target: https://github.com/OCA/product-attribute/tree/16.0/product_abc_classification :alt: OCA/product-attribute .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/product-attribute-16-0/product-attribute-16-0-product_abc_classification_base + :target: https://translation.odoo-community.org/projects/product-attribute-16-0/product-attribute-16-0-product_abc_classification :alt: Translate me on Weblate -.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/135/16.0 - :alt: Try me on Runbot +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/product-attribute&target_branch=16.0 + :alt: Try me on Runboat -|badge1| |badge2| |badge3| |badge4| |badge5| +|badge1| |badge2| |badge3| |badge4| |badge5| This modules provides the bases to build ABC analysis (or ABC classification) addons. These classification are used by inventory management teams to help @@ -62,8 +65,8 @@ 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 `_. +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. @@ -97,6 +100,6 @@ 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/product-attribute `_ project on GitHub. +This module is part of the `OCA/product-attribute `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_abc_classification/i18n/fr.po b/product_abc_classification/i18n/fr.po index dffcbb0a26c..dd41746496f 100644 --- a/product_abc_classification/i18n/fr.po +++ b/product_abc_classification/i18n/fr.po @@ -1,6 +1,6 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * product_abc_classification +# * product_abc_classification # msgid "" msgstr "" @@ -10,18 +10,19 @@ msgstr "" "PO-Revision-Date: 2021-02-15 16:46+0000\n" "Last-Translator: <>\n" "Language-Team: \n" +"Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_percentage +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level__percentage msgid "% Indicator" msgstr "% KPI" #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_percentage_products +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level__percentage_products msgid "% Products" msgstr "% Articles" @@ -30,6 +31,12 @@ msgstr "% Articles" msgid "ABC Classification" msgstr "Classification ABC" +#. module: product_abc_classification +#: model:ir.model,name:product_abc_classification.model_abc_classification_level +#, fuzzy +msgid "ABC Classification Level" +msgstr "Classe / Niveau" + #. module: product_abc_classification #: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_form_view msgid "ABC Classification Product Level" @@ -46,18 +53,18 @@ msgstr "Profils de classification ABC" msgid "ABC Profile" msgstr "Profil ABC" -#. module: product_abc_classification -#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_tree_view -msgid "ABC Profiles" -msgstr "Profils ABC" - #. module: product_abc_classification #: model:ir.model,name:product_abc_classification.model_abc_classification_product_level +#: model:ir.model.fields,field_description:product_abc_classification.field_product_product__abc_classification_product_level_ids +#: model:ir.model.fields,field_description:product_abc_classification.field_product_template__abc_classification_product_level_ids msgid "Abc Classification Product Level" msgstr "Niveau de classification" #. module: product_abc_classification #: model:ir.model,name:product_abc_classification.model_abc_classification_profile +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__allowed_profile_ids +#: model:ir.model.fields,field_description:product_abc_classification.field_product_product__abc_classification_profile_ids +#: model:ir.model.fields,field_description:product_abc_classification.field_product_template__abc_classification_profile_ids msgid "Abc Classification Profile" msgstr "Profil de classification ABC" @@ -67,37 +74,46 @@ msgid "Abc classification" msgstr "Classification ABC" #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_delivery_carrier_abc_classification_product_level_ids -#: model:ir.model.fields,field_description:product_abc_classification.field_product_product_abc_classification_product_level_ids -#: model:ir.model.fields,field_description:product_abc_classification.field_product_template_abc_classification_product_level_ids -msgid "Abc classification product level ids" -msgstr "Classes ABC" +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_needaction +msgid "Action Needed" +msgstr "" + +#. module: product_abc_classification +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_form_view +msgid "Additional Information" +msgstr "" #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_allowed_profile_ids -#: model:ir.model.fields,field_description:product_abc_classification.field_delivery_carrier_abc_classification_profile_ids -#: model:ir.model.fields,field_description:product_abc_classification.field_product_product_abc_classification_profile_ids -#: model:ir.model.fields,field_description:product_abc_classification.field_product_template_abc_classification_profile_ids -msgid "Abc classification profile ids" -msgstr "Profils ABC" +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_attachment_count +msgid "Attachment Count" +msgstr "" #. module: product_abc_classification -#: selection:abc.classification.profile,profile_type:0 -msgid "Based on the count of delivered sale order line by product" -msgstr "Basé sur le total des lignes de vente par article" +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__auto_apply_computed_value +#, fuzzy +msgid "Auto Apply Computed Value" +msgstr "Appliquer automatiquement la classification calculée" #. module: product_abc_classification -#: model:ir.model.fields,help:product_abc_classification.field_abc_classification_level_name +#: model:ir.model.fields,help:product_abc_classification.field_abc_classification_profile__auto_apply_computed_value +msgid "" +"Check this if you want to apply the computed level on each product that has " +"this profile." +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,help:product_abc_classification.field_abc_classification_level__name msgid "Classification A, B or C" msgstr "Classification A, B ou C" #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_level_id +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__level_id msgid "Classification level" msgstr "Classe / Niveau" #. module: product_abc_classification -#: code:addons/product_abc_classification/models/abc_classification_product_level.py:84 +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_product_level.py:0 #, python-format msgid "Classification level is mandatory" msgstr "La classe / niveau est obligatoire" @@ -108,7 +124,12 @@ 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_computed_level_id +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_form_view +msgid "Computation" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__computed_level_id msgid "Computed classification level" msgstr "Classe calculée" @@ -118,132 +139,237 @@ msgid "Computed level differs from the specified level" msgstr "La class calculée diverge de la valeur spécifiée" #. module: product_abc_classification -#: code:addons/product_abc_classification/models/abc_classification_product_level.py:90 +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_product_level.py:0 #, python-format -msgid "Computed level must be in the same classifiation profile as the one on the product level" -msgstr "La classe calculée doit utiliser le même profil de classification que celui défini sur le produit" +msgid "" +"Computed level must be in the same classifiation profile as the one on the " +"product level" +msgstr "" +"La classe calculée doit utiliser le même profil de classification que celui " +"défini sur le produit" #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_create_uid -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_create_uid -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_create_uid +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level__create_uid +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__create_uid +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__create_uid msgid "Created by" msgstr "Créé par" #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_create_date -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_create_date -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_create_date +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level__create_date +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__create_date +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__create_date msgid "Created on" msgstr "Créé le" #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_display_name -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_display_name -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_display_name +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level__display_name +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__display_name +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__display_name msgid "Display Name" msgstr "Nom affiché" +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + #. module: product_abc_classification #: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view msgid "Group By" msgstr "Grouper par" #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_id -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_id -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_id +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__has_message +msgid "Has Message" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level__id +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__id +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__id msgid "ID" msgstr "ID" #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_flag -msgid "If True, this means that the manual classification is different from the computed one" -msgstr "Si coché, indique que la classe attribuée manuellement au produit diverge de la classe calculée par le système." +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__flag +msgid "" +"If True, this means that the manual classification is different from the " +"computed one" +msgstr "" +"Si coché, indique que la classe attribuée manuellement au produit diverge de " +"la classe calculée par le système." + +#. module: product_abc_classification +#: model:ir.model.fields,help:product_abc_classification.field_abc_classification_product_level__message_needaction +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,help:product_abc_classification.field_abc_classification_product_level__message_has_error +#: model:ir.model.fields,help:product_abc_classification.field_abc_classification_product_level__message_has_sms_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_is_follower +msgid "Is Follower" +msgstr "" #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level___last_update -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level___last_update -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile___last_update +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level____last_update +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level____last_update +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile____last_update msgid "Last Modified on" msgstr "Dernière modification le" #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_write_uid -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_write_uid -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_write_uid +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level__write_uid +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__write_uid +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__write_uid msgid "Last Updated by" msgstr "Dernière mise à jour par" #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_write_date -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_write_date -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_write_date +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level__write_date +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__write_date +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__write_date msgid "Last Updated on" msgstr "Dernière mise à jour le" #. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__level_ids #: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view msgid "Level" msgstr "Niveau" #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_level_ids -msgid "Level ids" -msgstr "Classes" - -#. module: product_abc_classification -#: sql_constraint:abc.classification.level:0 -#: code:addons/product_abc_classification/models/abc_classification_level.py:30 +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_level.py:0 +#: model:ir.model.constraint,message:product_abc_classification.constraint_abc_classification_level_name_uniq #, python-format msgid "Level name must be unique by profile" msgstr "Le nom de la classe doit être unique par profil" #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_manual_level_id +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_form_view +#, fuzzy +msgid "Levels" +msgstr "Niveau" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_main_attachment_id +msgid "Main Attachment" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__manual_level_id msgid "Manual classification level" msgstr "Classe (Valeur à utiliser)" #. module: product_abc_classification -#: code:addons/product_abc_classification/models/abc_classification_product_level.py:100 +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_product_level.py:0 #, python-format -msgid "Manual level must be in the same classifiation profile as the one on the product level" -msgstr "La classe à utiliser doit utiliser le même profil de classification que celui défini sur le produit" +msgid "" +"Manual level must be in the same classifiation profile as the one on the " +"product level" +msgstr "" +"La classe à utiliser doit utiliser le même profil de classification que " +"celui défini sur le produit" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_ids +msgid "Messages" +msgstr "" #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_name -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_name +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level__name +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__name msgid "Name" msgstr "Nom" #. module: product_abc_classification -#: sql_constraint:abc.classification.product.level:0 -#: code:addons/product_abc_classification/models/abc_classification_product_level.py:76 +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,help:product_abc_classification.field_abc_classification_product_level__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,help:product_abc_classification.field_abc_classification_product_level__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: product_abc_classification +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_product_level.py:0 +#: model:ir.model.constraint,message:product_abc_classification.constraint_abc_classification_product_level_product_level_uniq #, python-format msgid "Only one level by profile by product allowed" msgstr "Une classe de classification ABC par profil et par produit autorisée." #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_period +#: model:ir.actions.server,name:product_abc_classification.ir_cron_product_abc_classification_ir_actions_server +#: model:ir.cron,cron_name:product_abc_classification.ir_cron_product_abc_classification +#, fuzzy +msgid "Perform the product ABC Classification" +msgstr "Classification ABC des articles" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__period msgid "Period on which to compute the classification (Days)" msgstr "Période référence pour le calcul de la classification (Nbr jours)" #. module: product_abc_classification -#: model:ir.model,name:product_abc_classification.model_product_product -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_product_id +#: model:ir.model,name:product_abc_classification.model_product_template +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__product_id msgid "Product" msgstr "Article" #. module: product_abc_classification -#: model:ir.model,name:product_abc_classification.model_product_template -msgid "Product Template" +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__product_count +#, fuzzy +msgid "Product Count" +msgstr "Article" + +#. module: product_abc_classification +#: model:ir.model,name:product_abc_classification.model_product_product +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__product_variant_ids +#, fuzzy +msgid "Product Variant" msgstr "Modèle de produit" #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_product_tmpl_id +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__product_tmpl_id msgid "Product template" msgstr "Modèle de produit" +#. module: product_abc_classification +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_form_view +#, fuzzy +msgid "Products" +msgstr "Article" + #. module: product_abc_classification #: model:ir.actions.act_window,name:product_abc_classification.abc_classification_product_level_action #: model:ir.ui.menu,name:product_abc_classification.menu_abc_classification_product_level_config_stock @@ -251,77 +377,117 @@ msgid "Products ABC Classification" msgstr "Classification ABC des articles" #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_profile_id +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level__profile_id +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__profile_id #: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view msgid "Profile" msgstr "Profil" #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level_profile_id -msgid "Profile id" +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_form_view +#, fuzzy +msgid "Profile Information" msgstr "Profil" #. module: product_abc_classification -#: sql_constraint:abc.classification.profile:0 -#: code:addons/product_abc_classification/models/abc_classification_profile.py:33 +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_profile.py:0 +#: model:ir.model.constraint,message:product_abc_classification.constraint_abc_classification_profile_name_uniq #, python-format msgid "Profile name must be unique" msgstr "Le nom du profil doit être unique" #. module: product_abc_classification -#: code:addons/product_abc_classification/models/abc_classification_level.py:39 +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_has_sms_error +msgid "SMS Delivery error" +msgstr "" + +#. module: product_abc_classification +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_level.py:0 #, python-format msgid "The percentage cannot be greater than 100." msgstr "Le pourcentage ne peut pas dépasser 100." #. module: product_abc_classification -#: code:addons/product_abc_classification/models/abc_classification_level.py:51 +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_level.py:0 #, python-format msgid "The percentage of products cannot be greater than 100." msgstr "Le pourcentage d'articles' ne peut pas dépasser 100." #. module: product_abc_classification -#: code:addons/product_abc_classification/models/abc_classification_level.py:55 +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_level.py:0 #, python-format msgid "The percentage of products should be a positive number." msgstr "Le pourcentage d'articles' doit être un nombre positif." #. module: product_abc_classification -#: code:addons/product_abc_classification/models/abc_classification_level.py:43 +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_level.py:0 #, python-format msgid "The percentage should be a positive number." msgstr "Le pourcentage doit être un nombre positif." #. module: product_abc_classification -#: code:addons/product_abc_classification/models/abc_classification_profile.py:52 +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_profile.py:0 #, python-format msgid "The percentages of the levels must be unique." -msgstr "Les valeurs de pourcentage des différentes classes doivent être uniques pour un même profil." +msgstr "" +"Les valeurs de pourcentage des différentes classes doivent être uniques pour " +"un même profil." #. module: product_abc_classification -#: code:addons/product_abc_classification/models/abc_classification_profile.py:43 +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_profile.py:0 #, python-format msgid "The sum of the percentages of the levels should be 100." msgstr "La somme des pourcentages ne doit pas dépasser 100." #. module: product_abc_classification -#: code:addons/product_abc_classification/models/abc_classification_profile.py:60 +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_profile.py:0 #, python-format msgid "The sum of the products percentages of the levels should be 100." msgstr "La somme des pourcentages d'articles ne doit pas dépasser 100." #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level_profile_type -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_profile_type +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__profile_type +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__profile_type msgid "Type of ABC classification" msgstr "Type de classification ABC" #. module: product_abc_classification -#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile_auto_apply_computed_value -msgid "Auto apply computed value" -msgstr "Appliquer automatiquement la classification calculée" +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__website_message_ids +msgid "Website Messages" +msgstr "" #. module: product_abc_classification -#: model:ir.model,name:product_abc_classification.model_abc_classification_level -msgid "abc.classification.level" -msgstr "Classe de classification ABC" +#: model:ir.model.fields,help:product_abc_classification.field_abc_classification_product_level__website_message_ids +msgid "Website communication history" +msgstr "" + +#. module: product_abc_classification +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_form_view +msgid "e.g. Sale Profile" +msgstr "" + +#~ msgid "ABC Profiles" +#~ msgstr "Profils ABC" + +#~ msgid "Abc classification product level ids" +#~ msgstr "Classes ABC" + +#~ msgid "Abc classification profile ids" +#~ msgstr "Profils ABC" + +#~ msgid "Based on the count of delivered sale order line by product" +#~ msgstr "Basé sur le total des lignes de vente par article" + +#~ msgid "Level ids" +#~ msgstr "Classes" + +#~ msgid "abc.classification.level" +#~ msgstr "Classe de classification ABC" diff --git a/product_abc_classification/i18n/product_abc_classification.pot b/product_abc_classification/i18n/product_abc_classification.pot new file mode 100644 index 00000000000..41db7ec3115 --- /dev/null +++ b/product_abc_classification/i18n/product_abc_classification.pot @@ -0,0 +1,456 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_abc_classification +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.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: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level__percentage +msgid "% Indicator" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level__percentage_products +msgid "% Products" +msgstr "" + +#. module: product_abc_classification +#: model_terms:ir.ui.view,arch_db:product_abc_classification.product_template_form_view +msgid "ABC Classification" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model,name:product_abc_classification.model_abc_classification_level +msgid "ABC Classification Level" +msgstr "" + +#. module: product_abc_classification +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_form_view +msgid "ABC Classification Product Level" +msgstr "" + +#. module: product_abc_classification +#: model:ir.actions.act_window,name:product_abc_classification.abc_classification_profile_action +#: model:ir.ui.menu,name:product_abc_classification.menu_abc_classification_profile_config_stock +msgid "ABC Classification profiles" +msgstr "" + +#. module: product_abc_classification +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_form_view +msgid "ABC Profile" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model,name:product_abc_classification.model_abc_classification_product_level +#: model:ir.model.fields,field_description:product_abc_classification.field_product_product__abc_classification_product_level_ids +#: model:ir.model.fields,field_description:product_abc_classification.field_product_template__abc_classification_product_level_ids +msgid "Abc Classification Product Level" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model,name:product_abc_classification.model_abc_classification_profile +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__allowed_profile_ids +#: model:ir.model.fields,field_description:product_abc_classification.field_product_product__abc_classification_profile_ids +#: model:ir.model.fields,field_description:product_abc_classification.field_product_template__abc_classification_profile_ids +msgid "Abc Classification Profile" +msgstr "" + +#. module: product_abc_classification +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view +msgid "Abc classification" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_needaction +msgid "Action Needed" +msgstr "" + +#. module: product_abc_classification +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_form_view +msgid "Additional Information" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_attachment_count +msgid "Attachment Count" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__auto_apply_computed_value +msgid "Auto Apply Computed Value" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,help:product_abc_classification.field_abc_classification_profile__auto_apply_computed_value +msgid "" +"Check this if you want to apply the computed level on each product that has " +"this profile." +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,help:product_abc_classification.field_abc_classification_level__name +msgid "Classification A, B or C" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__level_id +msgid "Classification level" +msgstr "" + +#. module: product_abc_classification +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_product_level.py:0 +#, python-format +msgid "Classification level is mandatory" +msgstr "" + +#. module: product_abc_classification +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view +msgid "Classification not in sync with computed" +msgstr "" + +#. module: product_abc_classification +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_form_view +msgid "Computation" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__computed_level_id +msgid "Computed classification level" +msgstr "" + +#. module: product_abc_classification +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_form_view +msgid "Computed level differs from the specified level" +msgstr "" + +#. module: product_abc_classification +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_product_level.py:0 +#, python-format +msgid "" +"Computed level must be in the same classifiation profile as the one on the " +"product level" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level__create_uid +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__create_uid +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__create_uid +msgid "Created by" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level__create_date +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__create_date +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__create_date +msgid "Created on" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level__display_name +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__display_name +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__display_name +msgid "Display Name" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: product_abc_classification +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view +msgid "Group By" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__has_message +msgid "Has Message" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level__id +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__id +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__id +msgid "ID" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__flag +msgid "" +"If True, this means that the manual classification is different from the " +"computed one" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,help:product_abc_classification.field_abc_classification_product_level__message_needaction +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,help:product_abc_classification.field_abc_classification_product_level__message_has_error +#: model:ir.model.fields,help:product_abc_classification.field_abc_classification_product_level__message_has_sms_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_is_follower +msgid "Is Follower" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level____last_update +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level____last_update +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile____last_update +msgid "Last Modified on" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level__write_uid +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__write_uid +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level__write_date +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__write_date +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__write_date +msgid "Last Updated on" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__level_ids +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view +msgid "Level" +msgstr "" + +#. module: product_abc_classification +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_level.py:0 +#: model:ir.model.constraint,message:product_abc_classification.constraint_abc_classification_level_name_uniq +#, python-format +msgid "Level name must be unique by profile" +msgstr "" + +#. module: product_abc_classification +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_form_view +msgid "Levels" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_main_attachment_id +msgid "Main Attachment" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__manual_level_id +msgid "Manual classification level" +msgstr "" + +#. module: product_abc_classification +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_product_level.py:0 +#, python-format +msgid "" +"Manual level must be in the same classifiation profile as the one on the " +"product level" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_ids +msgid "Messages" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level__name +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__name +msgid "Name" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,help:product_abc_classification.field_abc_classification_product_level__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,help:product_abc_classification.field_abc_classification_product_level__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: product_abc_classification +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_product_level.py:0 +#: model:ir.model.constraint,message:product_abc_classification.constraint_abc_classification_product_level_product_level_uniq +#, python-format +msgid "Only one level by profile by product allowed" +msgstr "" + +#. module: product_abc_classification +#: model:ir.actions.server,name:product_abc_classification.ir_cron_product_abc_classification_ir_actions_server +#: model:ir.cron,cron_name:product_abc_classification.ir_cron_product_abc_classification +msgid "Perform the product ABC Classification" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__period +msgid "Period on which to compute the classification (Days)" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model,name:product_abc_classification.model_product_template +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__product_id +msgid "Product" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__product_count +msgid "Product Count" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model,name:product_abc_classification.model_product_product +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__product_variant_ids +msgid "Product Variant" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__product_tmpl_id +msgid "Product template" +msgstr "" + +#. module: product_abc_classification +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_form_view +msgid "Products" +msgstr "" + +#. module: product_abc_classification +#: model:ir.actions.act_window,name:product_abc_classification.abc_classification_product_level_action +#: model:ir.ui.menu,name:product_abc_classification.menu_abc_classification_product_level_config_stock +msgid "Products ABC Classification" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_level__profile_id +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__profile_id +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_product_level_search_view +msgid "Profile" +msgstr "" + +#. module: product_abc_classification +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_form_view +msgid "Profile Information" +msgstr "" + +#. module: product_abc_classification +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_profile.py:0 +#: model:ir.model.constraint,message:product_abc_classification.constraint_abc_classification_profile_name_uniq +#, python-format +msgid "Profile name must be unique" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__message_has_sms_error +msgid "SMS Delivery error" +msgstr "" + +#. module: product_abc_classification +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_level.py:0 +#, python-format +msgid "The percentage cannot be greater than 100." +msgstr "" + +#. module: product_abc_classification +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_level.py:0 +#, python-format +msgid "The percentage of products cannot be greater than 100." +msgstr "" + +#. module: product_abc_classification +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_level.py:0 +#, python-format +msgid "The percentage of products should be a positive number." +msgstr "" + +#. module: product_abc_classification +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_level.py:0 +#, python-format +msgid "The percentage should be a positive number." +msgstr "" + +#. module: product_abc_classification +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_profile.py:0 +#, python-format +msgid "The percentages of the levels must be unique." +msgstr "" + +#. module: product_abc_classification +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_profile.py:0 +#, python-format +msgid "The sum of the percentages of the levels should be 100." +msgstr "" + +#. module: product_abc_classification +#. odoo-python +#: code:addons/product_abc_classification/models/abc_classification_profile.py:0 +#, python-format +msgid "The sum of the products percentages of the levels should be 100." +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__profile_type +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_profile__profile_type +msgid "Type of ABC classification" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,field_description:product_abc_classification.field_abc_classification_product_level__website_message_ids +msgid "Website Messages" +msgstr "" + +#. module: product_abc_classification +#: model:ir.model.fields,help:product_abc_classification.field_abc_classification_product_level__website_message_ids +msgid "Website communication history" +msgstr "" + +#. module: product_abc_classification +#: model_terms:ir.ui.view,arch_db:product_abc_classification.abc_classification_profile_form_view +msgid "e.g. Sale Profile" +msgstr "" diff --git a/product_abc_classification/static/description/index.html b/product_abc_classification/static/description/index.html index b309d87953d..171c5b550d4 100644 --- a/product_abc_classification/static/description/index.html +++ b/product_abc_classification/static/description/index.html @@ -1,20 +1,20 @@ - + - + Product Abc Classification -
-

Product Abc Classification

+
+ + +Odoo Community Association + +
+

Product Abc Classification

-

Beta License: AGPL-3 OCA/product-attribute Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/product-attribute Translate me on Weblate Try me on Runboat

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 @@ -388,15 +393,16 @@

Product Abc Classification

  • Credits
  • -

    Usage

    +

    Usage

    To use this module, you need to:

    -

    #. Go to Sales or Inventory menu, then to Configuration/Products/ABC +

    #. Go to Inventory menu, then to Configuration/Products/ABC Classification Profile and create a profile with levels, knowing that the sum of all levels in the profile should sum 100 and all the levels should be different.

    @@ -407,7 +413,7 @@

    Usage

    child categories and products will be profiled (or unprofiled).

    -

    Bug Tracker

    +

    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 @@ -415,25 +421,31 @@

    Bug Tracker

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

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • ACSONE SA/NV
    • ForgeFlow
    -

    Contributors

    +

    Contributors

    +
    +

    Other credits

    +

    The migration of this module from 17.0 to 18.0 was financially supported +by Camptocamp

    +
    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association @@ -446,5 +458,6 @@

    Maintainers

    +
    diff --git a/product_abc_classification/tests/common.py b/product_abc_classification/tests/common.py index 899d55dc7b4..1311f3f2c65 100644 --- a/product_abc_classification/tests/common.py +++ b/product_abc_classification/tests/common.py @@ -15,6 +15,10 @@ def setUpClass(cls): cls.ABCClassificationProfile._fields["profile_type"].selection = [ ("test_type", "Test Type") ] + cls.ABCClassificationProfile._fields["profile_type"]._selection = { + "test_type", + "Test Type", + } cls.classification_profile = cls.ABCClassificationProfile.create( {"name": "Profile test", "profile_type": "test_type"} ) diff --git a/product_abc_classification/views/abc_classification_product_level.xml b/product_abc_classification/views/abc_classification_product_level.xml index a1059740359..9629c8efa8b 100644 --- a/product_abc_classification/views/abc_classification_product_level.xml +++ b/product_abc_classification/views/abc_classification_product_level.xml @@ -31,10 +31,7 @@ -
    - - -
    +
    @@ -44,13 +41,13 @@ >abc.classification.product.level.tree (in product_abc_classification) abc.classification.product.level - + - + @@ -89,7 +86,7 @@ Products ABC Classification abc.classification.product.level - tree,form + list,form {'search_default_group_by_level': 1} - + - + @@ -65,15 +65,15 @@ >abc.classification.profile.tree (in product_abc_classification) abc.classification.profile - + - + ABC Classification profiles abc.classification.profile - tree,form + list,form From 1b8be04b53afc9f5bda168cd5f735b46c1bc16f9 Mon Sep 17 00:00:00 2001 From: Ruchir Shukla Date: Fri, 26 Dec 2025 18:57:53 +0530 Subject: [PATCH 22/22] [MIG] product_abc_classification: Migration to 19.0 --- product_abc_classification/README.rst | 10 +++++----- product_abc_classification/__manifest__.py | 2 +- .../models/abc_classification_level.py | 11 ++++------- .../models/abc_classification_product_level.py | 11 ++++------- .../models/abc_classification_profile.py | 4 ++-- .../static/description/index.html | 6 +++--- product_abc_classification/tests/common.py | 7 ++++++- .../tests/test_abc_classification_product_level.py | 3 --- .../views/abc_classification_product_level.xml | 2 +- 9 files changed, 26 insertions(+), 30 deletions(-) diff --git a/product_abc_classification/README.rst b/product_abc_classification/README.rst index cc67bd9ddd4..688c72c15fe 100644 --- a/product_abc_classification/README.rst +++ b/product_abc_classification/README.rst @@ -21,13 +21,13 @@ Product Abc Classification :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 - :target: https://github.com/OCA/product-attribute/tree/18.0/product_abc_classification + :target: https://github.com/OCA/product-attribute/tree/19.0/product_abc_classification :alt: OCA/product-attribute .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/product-attribute-18-0/product-attribute-18-0-product_abc_classification + :target: https://translation.odoo-community.org/projects/product-attribute-19-0/product-attribute-19-0-product_abc_classification :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/product-attribute&target_branch=18.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/product-attribute&target_branch=19.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -73,7 +73,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -114,6 +114,6 @@ 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/product-attribute `_ project on GitHub. +This module is part of the `OCA/product-attribute `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_abc_classification/__manifest__.py b/product_abc_classification/__manifest__.py index e4a4ea074ab..fd5e7c90670 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.1.0.0", + "version": "19.0.1.0.0", "license": "AGPL-3", "author": "ACSONE SA/NV, ForgeFlow, Odoo Community Association (OCA)", "website": "https://github.com/OCA/product-attribute", diff --git a/product_abc_classification/models/abc_classification_level.py b/product_abc_classification/models/abc_classification_level.py index 2189024d3d1..5052a077e64 100644 --- a/product_abc_classification/models/abc_classification_level.py +++ b/product_abc_classification/models/abc_classification_level.py @@ -18,13 +18,10 @@ class AbcClassificationLevel(models.Model): name = fields.Char(help="Classification A, B or C", required=True) - _sql_constraints = [ - ( - "name_uniq", - "UNIQUE(profile_id, name)", - "Level name must be unique by profile", - ) - ] + _name_uniq = models.Constraint( + "UNIQUE(profile_id, name)", + "Level name must be unique by profile", + ) @api.constrains("percentage") def _check_percentage(self): diff --git a/product_abc_classification/models/abc_classification_product_level.py b/product_abc_classification/models/abc_classification_product_level.py index 505642afb2d..33559296bc2 100644 --- a/product_abc_classification/models/abc_classification_product_level.py +++ b/product_abc_classification/models/abc_classification_product_level.py @@ -66,13 +66,10 @@ class AbcClassificationProductLevel(models.Model): related="product_id.abc_classification_profile_ids", ) - _sql_constraints = [ - ( - "product_level_uniq", - "UNIQUE(profile_id, product_id)", - "Only one level by profile by product allowed", - ) - ] + _product_level_uniq = models.Constraint( + "UNIQUE(profile_id, product_id)", + "Only one level by profile by product allowed", + ) @api.constrains("computed_level_id", "manual_level_id", "product_id") def _check_level(self): diff --git a/product_abc_classification/models/abc_classification_profile.py b/product_abc_classification/models/abc_classification_profile.py index 996186af797..0d43604c1b4 100644 --- a/product_abc_classification/models/abc_classification_profile.py +++ b/product_abc_classification/models/abc_classification_profile.py @@ -45,7 +45,7 @@ class AbcClassificationProfile(models.Model): "profile.", ) - _sql_constraints = [("name_uniq", "UNIQUE(name)", "Profile name must be unique")] + _name_uniq = models.Constraint("UNIQUE(name)", "Profile name must be unique") @api.constrains("level_ids") def _check_levels(self): @@ -105,7 +105,7 @@ def action_view_products(self): @api.model def _cron_compute_abc_classification(self): - self.search([])._compute_abc_classification() + self.search([], limit=False)._compute_abc_classification() def write(self, vals): res = super().write(vals) diff --git a/product_abc_classification/static/description/index.html b/product_abc_classification/static/description/index.html index bfaff2880f6..29140cf59a0 100644 --- a/product_abc_classification/static/description/index.html +++ b/product_abc_classification/static/description/index.html @@ -374,7 +374,7 @@

    Product Abc Classification

    !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:f602d3cbe4b034e608e4a5b554b3532e87e682d94cb1ee8ba499905258e348c3 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

    Beta License: AGPL-3 OCA/product-attribute Translate me on Weblate Try me on Runboat

    +

    Beta License: AGPL-3 OCA/product-attribute Translate me on Weblate Try me on Runboat

    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 @@ -417,7 +417,7 @@

    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.

    +feedback.

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

    @@ -453,7 +453,7 @@

    Maintainers

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

    +

    This module is part of the OCA/product-attribute project on GitHub.

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

    diff --git a/product_abc_classification/tests/common.py b/product_abc_classification/tests/common.py index 1311f3f2c65..29ba1a8b461 100644 --- a/product_abc_classification/tests/common.py +++ b/product_abc_classification/tests/common.py @@ -102,11 +102,16 @@ def setUpClass(cls): cls.size_attr_value_s = cls.size_attr.value_ids[0] cls.size_attr_value_m = cls.size_attr.value_ids[1] cls.uom_unit = cls.env.ref("uom.product_uom_unit") + cls.category_all = cls.env["product.category"].create( + { + "name": "All", + } + ) cls.product_template = cls.env["product.template"].create( { "name": "Test sized", "uom_id": cls.uom_unit.id, - "uom_po_id": cls.uom_unit.id, + "categ_id": cls.category_all.id, "attribute_line_ids": [ ( 0, diff --git a/product_abc_classification/tests/test_abc_classification_product_level.py b/product_abc_classification/tests/test_abc_classification_product_level.py index b995424feb0..18d7786cf71 100644 --- a/product_abc_classification/tests/test_abc_classification_product_level.py +++ b/product_abc_classification/tests/test_abc_classification_product_level.py @@ -17,7 +17,6 @@ def setUpClass(cls): { "name": "Test 1", "uom_id": cls.uom_unit.id, - "uom_po_id": cls.uom_unit.id, } ) cls.product_level = cls.ProductLevel.create( @@ -34,7 +33,6 @@ def _create_product_levels(cls): { "name": "Test 2", "uom_id": cls.uom_unit.id, - "uom_po_id": cls.uom_unit.id, } ) @@ -42,7 +40,6 @@ def _create_product_levels(cls): { "name": "Test 3", "uom_id": cls.uom_unit.id, - "uom_po_id": cls.uom_unit.id, } ) cls.ProductLevel.create( diff --git a/product_abc_classification/views/abc_classification_product_level.xml b/product_abc_classification/views/abc_classification_product_level.xml index 9629c8efa8b..c131a985596 100644 --- a/product_abc_classification/views/abc_classification_product_level.xml +++ b/product_abc_classification/views/abc_classification_product_level.xml @@ -66,7 +66,7 @@ name="has_flag" domain="[('flag','=',True)]" /> - +