From 959040b2cb9299b2670621df6380846049881a71 Mon Sep 17 00:00:00 2001 From: mreficent Date: Thu, 28 May 2020 13:35:47 +0200 Subject: [PATCH 01/21] [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 000000000000..0650744f6bc6 --- /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 000000000000..d67fd6bc574d --- /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 000000000000..36faa7971a5e --- /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 000000000000..39394627c878 --- /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 000000000000..2b6790c6b5be --- /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 000000000000..f75be6575589 --- /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 000000000000..df7348e554cb --- /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 000000000000..99ce94704818 --- /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 000000000000..2e34e218a547 --- /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 000000000000..b55e108765fd --- /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 000000000000..a66526e486c7 --- /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 000000000000..421beb328a90 --- /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 000000000000..001fd3b21e63 --- /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 000000000000..5caf6a28283a --- /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 9b4a0a427721598810b814c592537578b3924b9c Mon Sep 17 00:00:00 2001 From: Lindsay Marion Date: Tue, 26 Jan 2021 12:08:02 +0100 Subject: [PATCH 02/21] [IMP] product_abc_classification: Makes the classification computation logic pluggable --- product_abc_classification/__manifest__.py | 20 -- .../models/abc_classification_level.py | 35 --- .../models/abc_classification_profile.py | 156 ---------- .../models/product_product.py | 74 ----- .../views/abc_classification_view.xml | 69 ----- .../views/product_view.xml | 94 ------ .../__init__.py | 0 .../__manifest__.py | 19 ++ .../data/ir_cron.xml | 0 .../models/__init__.py | 7 +- .../models/abc_classification_level.py | 26 ++ .../abc_classification_product_level.py | 47 +++ .../models/abc_classification_profile.py | 36 +++ .../models/product_category.py | 0 .../models/product_product.py | 21 ++ .../models/product_template.py | 17 ++ .../readme/CONTRIBUTORS.rst | 0 .../readme/DESCRIPTION.rst | 0 .../readme/USAGE.rst | 0 .../security/ir.model.access.csv | 0 .../static/description/icon.png | Bin .../views/abc_classification_profile.xml | 62 ++++ .../views/product_template.xml | 40 +++ .../README.rst | 91 ++++++ .../__init__.py | 1 + .../__manifest__.py | 21 ++ .../models/__init__.py | 3 + .../models/abc_classification_level.py | 10 + .../models/abc_classification_profile.py | 144 +++++++++ .../models/product_template.py | 10 + .../static/description/icon.png | Bin 0 -> 9455 bytes .../tests/__init__.py | 1 + .../tests/test_abc_classification_profile.py | 273 ++++++++++++++++++ 33 files changed, 826 insertions(+), 451 deletions(-) delete mode 100644 product_abc_classification/__manifest__.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_product.py delete mode 100644 product_abc_classification/views/abc_classification_view.xml delete mode 100644 product_abc_classification/views/product_view.xml rename {product_abc_classification => product_abc_classification_base}/__init__.py (100%) create mode 100644 product_abc_classification_base/__manifest__.py rename {product_abc_classification => product_abc_classification_base}/data/ir_cron.xml (100%) rename {product_abc_classification => product_abc_classification_base}/models/__init__.py (58%) create mode 100644 product_abc_classification_base/models/abc_classification_level.py create mode 100644 product_abc_classification_base/models/abc_classification_product_level.py create mode 100644 product_abc_classification_base/models/abc_classification_profile.py rename {product_abc_classification => product_abc_classification_base}/models/product_category.py (100%) create mode 100644 product_abc_classification_base/models/product_product.py create mode 100644 product_abc_classification_base/models/product_template.py rename {product_abc_classification => product_abc_classification_base}/readme/CONTRIBUTORS.rst (100%) rename {product_abc_classification => product_abc_classification_base}/readme/DESCRIPTION.rst (100%) rename {product_abc_classification => product_abc_classification_base}/readme/USAGE.rst (100%) rename {product_abc_classification => product_abc_classification_base}/security/ir.model.access.csv (100%) rename {product_abc_classification => product_abc_classification_base}/static/description/icon.png (100%) create mode 100644 product_abc_classification_base/views/abc_classification_profile.xml create mode 100644 product_abc_classification_base/views/product_template.xml create mode 100644 product_abc_classification_sale_stock/README.rst create mode 100644 product_abc_classification_sale_stock/__init__.py create mode 100644 product_abc_classification_sale_stock/__manifest__.py create mode 100644 product_abc_classification_sale_stock/models/__init__.py create mode 100644 product_abc_classification_sale_stock/models/abc_classification_level.py create mode 100644 product_abc_classification_sale_stock/models/abc_classification_profile.py create mode 100644 product_abc_classification_sale_stock/models/product_template.py create mode 100644 product_abc_classification_sale_stock/static/description/icon.png create mode 100644 product_abc_classification_sale_stock/tests/__init__.py create mode 100644 product_abc_classification_sale_stock/tests/test_abc_classification_profile.py diff --git a/product_abc_classification/__manifest__.py b/product_abc_classification/__manifest__.py deleted file mode 100644 index d67fd6bc574d..000000000000 --- 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/models/abc_classification_level.py b/product_abc_classification/models/abc_classification_level.py deleted file mode 100644 index 2b6790c6b5be..000000000000 --- 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 f75be6575589..000000000000 --- 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_product.py b/product_abc_classification/models/product_product.py deleted file mode 100644 index 99ce94704818..000000000000 --- 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/views/abc_classification_view.xml b/product_abc_classification/views/abc_classification_view.xml deleted file mode 100644 index 001fd3b21e63..000000000000 --- 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 5caf6a28283a..000000000000 --- 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 - - - - - - - - diff --git a/product_abc_classification/__init__.py b/product_abc_classification_base/__init__.py similarity index 100% rename from product_abc_classification/__init__.py rename to product_abc_classification_base/__init__.py diff --git a/product_abc_classification_base/__manifest__.py b/product_abc_classification_base/__manifest__.py new file mode 100644 index 000000000000..2b8ccf1ffee8 --- /dev/null +++ b/product_abc_classification_base/__manifest__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Alc Product Abc Classification", + "summary": """ + ABC classification for sales and warehouse management""", + "version": "10.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "depends": ["product"], + "data": [ + "views/product_category.xml", + "views/product_template.xml", + "views/abc_classification_profile.xml", + ], + "demo": [], +} diff --git a/product_abc_classification/data/ir_cron.xml b/product_abc_classification_base/data/ir_cron.xml similarity index 100% rename from product_abc_classification/data/ir_cron.xml rename to product_abc_classification_base/data/ir_cron.xml diff --git a/product_abc_classification/models/__init__.py b/product_abc_classification_base/models/__init__.py similarity index 58% rename from product_abc_classification/models/__init__.py rename to product_abc_classification_base/models/__init__.py index 39394627c878..5fa33c2bb220 100644 --- a/product_abc_classification/models/__init__.py +++ b/product_abc_classification_base/models/__init__.py @@ -1,4 +1,5 @@ -from . import product_category -from . import product_product -from . import abc_classification_level from . import abc_classification_profile +from . import abc_classification_level +from . import product_template +from . import product_product +from . import abc_product_classification_level diff --git a/product_abc_classification_base/models/abc_classification_level.py b/product_abc_classification_base/models/abc_classification_level.py new file mode 100644 index 000000000000..c83a32271160 --- /dev/null +++ b/product_abc_classification_base/models/abc_classification_level.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# 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" + _order = "percentage desc, id desc" + + percentage = fields.Float(default=0.0, required=True, string="%") + profile_id = fields.Many2one("abc.classification.profile") + + name = fields.Char(help="Classification A, B or C") # unique par 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.")) + diff --git a/product_abc_classification_base/models/abc_classification_product_level.py b/product_abc_classification_base/models/abc_classification_product_level.py new file mode 100644 index 000000000000..462c6637a29f --- /dev/null +++ b/product_abc_classification_base/models/abc_classification_product_level.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class AbcProductClassificationLevel(models.Model): + + _name = "abc.product.classification.level" + _description = "Abc Product Classification Level" + + name = fields.Char() + + manual_level_id = fields.Many2one( + "abc.classification.level", string="Manual classification level" + ) + computed_level_id = fields.Many2one( + "abc.classification.level", string="Computed classification level" + ) + level_id = fields.Many2one( + "abc.classification.level", + string="Classification level", + compute="_compute_level_id", + ) + flag = fields.Boolean( + default=False, + compute="_compute_flag", + string="If True, this means that the manual classification is different from the computed one", + ) + product_id = fields.Many2one("product.product", string="Product", index=True) + # percentage + profile_id = fields.Many2one("abc.classification.profile", string="Profile") + + @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: + if rec.manual_level_id != rec.computed_level_id: + rec.flag = True diff --git a/product_abc_classification_base/models/abc_classification_profile.py b/product_abc_classification_base/models/abc_classification_profile.py new file mode 100644 index 000000000000..fa3c3cdc8b90 --- /dev/null +++ b/product_abc_classification_base/models/abc_classification_profile.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class AbcClassificationProfile(models.Model): + + _name = "abc.classification.profile" + _description = "Abc Classification Profile" + + name = fields.Char() + level_ids = fields.Many2many( + comodel_name="abc.classification.level", inverse_name="profile_id" + ) + display_name = fields.Char() + + 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, + ) + + def _fill_initial_product_data(self, date): + raise NotImplementedError() + + def _fill_data_(self, date, product_list): + raise NotImplementedError() + + @api.model + def _compute_abc_classification(self): + raise NotImplementedError() diff --git a/product_abc_classification/models/product_category.py b/product_abc_classification_base/models/product_category.py similarity index 100% rename from product_abc_classification/models/product_category.py rename to product_abc_classification_base/models/product_category.py diff --git a/product_abc_classification_base/models/product_product.py b/product_abc_classification_base/models/product_product.py new file mode 100644 index 000000000000..2632c1d6fad7 --- /dev/null +++ b/product_abc_classification_base/models/product_product.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# 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_product_classification_level_ids = fields.Many2many( + "abc.product.classification.level", index=True + ) + 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_base/models/product_template.py b/product_abc_classification_base/models/product_template.py new file mode 100644 index 000000000000..952cb26f6879 --- /dev/null +++ b/product_abc_classification_base/models/product_template.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ProductTemplate(models.Model): + + _inherit = "product.template" + + abc_product_classification_level_ids = fields.Many2many( + related="product_variant_ids.abc_product_classification_level_ids" + ) + abc_classification_profile_ids = fields.Many2many( + related="product_variant_ids.abc_classification_profile_ids" + ) diff --git a/product_abc_classification/readme/CONTRIBUTORS.rst b/product_abc_classification_base/readme/CONTRIBUTORS.rst similarity index 100% rename from product_abc_classification/readme/CONTRIBUTORS.rst rename to product_abc_classification_base/readme/CONTRIBUTORS.rst diff --git a/product_abc_classification/readme/DESCRIPTION.rst b/product_abc_classification_base/readme/DESCRIPTION.rst similarity index 100% rename from product_abc_classification/readme/DESCRIPTION.rst rename to product_abc_classification_base/readme/DESCRIPTION.rst diff --git a/product_abc_classification/readme/USAGE.rst b/product_abc_classification_base/readme/USAGE.rst similarity index 100% rename from product_abc_classification/readme/USAGE.rst rename to product_abc_classification_base/readme/USAGE.rst diff --git a/product_abc_classification/security/ir.model.access.csv b/product_abc_classification_base/security/ir.model.access.csv similarity index 100% rename from product_abc_classification/security/ir.model.access.csv rename to product_abc_classification_base/security/ir.model.access.csv diff --git a/product_abc_classification/static/description/icon.png b/product_abc_classification_base/static/description/icon.png similarity index 100% rename from product_abc_classification/static/description/icon.png rename to product_abc_classification_base/static/description/icon.png diff --git a/product_abc_classification_base/views/abc_classification_profile.xml b/product_abc_classification_base/views/abc_classification_profile.xml new file mode 100644 index 000000000000..a6c22d0689ff --- /dev/null +++ b/product_abc_classification_base/views/abc_classification_profile.xml @@ -0,0 +1,62 @@ + + + + + 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_base/views/product_template.xml b/product_abc_classification_base/views/product_template.xml new file mode 100644 index 000000000000..6ae2aa3c49c8 --- /dev/null +++ b/product_abc_classification_base/views/product_template.xml @@ -0,0 +1,40 @@ + + + + + product.template.tree + product.template + + + + + + + + + product.template.form (ABC Classification) + product.template + + + + + + + + + + + + + + + diff --git a/product_abc_classification_sale_stock/README.rst b/product_abc_classification_sale_stock/README.rst new file mode 100644 index 000000000000..2e60ba3a5a43 --- /dev/null +++ b/product_abc_classification_sale_stock/README.rst @@ -0,0 +1,91 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +======================================== +Alc Product Abc Classification Warehouse +======================================== + +Get ABC classification for warehouse type + +Installation +============ + +To install this module, you need to: + +#. Do this ... + +Configuration +============= + +To configure this module, you need to: + +#. Go to ... + +.. figure:: path/to/local/image.png + :alt: alternative description + :width: 600 px + +Usage +===== + +To use this module, you need to: + +#. Go to ... + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/{repo_id}/{branch} + +.. repo_id is available in https://github.com/OCA/maintainer-tools/blob/master/tools/repos_with_ids.txt +.. branch is "8.0" for example + +Known issues / Roadmap +====================== + +* ... + +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 smash it by providing detailed and welcomed feedback. + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Firstname Lastname +* Second Person + +Funders +------- + +The development of this module has been financially supported by: + +* Company 1 name +* Company 2 name + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +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. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/product_abc_classification_sale_stock/__init__.py b/product_abc_classification_sale_stock/__init__.py new file mode 100644 index 000000000000..0650744f6bc6 --- /dev/null +++ b/product_abc_classification_sale_stock/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/product_abc_classification_sale_stock/__manifest__.py b/product_abc_classification_sale_stock/__manifest__.py new file mode 100644 index 000000000000..88b690f8aade --- /dev/null +++ b/product_abc_classification_sale_stock/__manifest__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Alc Product Abc Classification Warehouse", + "summary": """ + Get ABC classification for warehouse type""", + "version": "10.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "depends": [ + "alc_product_abc_classification", + "product", + "sale_stock", + "stock_picking_subcode", + "stock_delivery_note", + ], + "data": [], + "demo": [], +} diff --git a/product_abc_classification_sale_stock/models/__init__.py b/product_abc_classification_sale_stock/models/__init__.py new file mode 100644 index 000000000000..424bcbb81ccb --- /dev/null +++ b/product_abc_classification_sale_stock/models/__init__.py @@ -0,0 +1,3 @@ +from . import abc_classification_profile +from . import abc_classification_level +from . import product_template diff --git a/product_abc_classification_sale_stock/models/abc_classification_level.py b/product_abc_classification_sale_stock/models/abc_classification_level.py new file mode 100644 index 000000000000..cd72ba44dda6 --- /dev/null +++ b/product_abc_classification_sale_stock/models/abc_classification_level.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class AbcClassificationLevel(models.Model): + + _inherit = "abc.classification.level" diff --git a/product_abc_classification_sale_stock/models/abc_classification_profile.py b/product_abc_classification_sale_stock/models/abc_classification_profile.py new file mode 100644 index 000000000000..7a51671928e7 --- /dev/null +++ b/product_abc_classification_sale_stock/models/abc_classification_profile.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import datetime, timedelta + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class AbcClassificationProfile(models.Model): + + _inherit = "abc.classification.profile" + + profile_type = fields.Selection(selection_add=[("stock", "Stock")]) + + def _fill_initial_product_data(self, date): + product_list = [] + if self.profile_type == "stock": + return self._fill_data(date, product_list) + return product_list, 0 + + def _fill_data(self, date, product_list): + self.ensure_one() + warehouse = self.env.ref("stock.warehouse0") + self.env.cr.execute( + """ SELECT sol.product_id product_id, so.warehouse_id warehouse_id, COUNT(sol.id) number_of_so_lines + FROM + sale_order so + JOIN + sale_order_line sol ON sol.order_id = so.id + JOIN + stock_move sm ON sol.id = sm.order_line_id + JOIN + abc_classification_profile_product_rel rel ON rel.product_id = sol.product_id + WHERE sol.qty_delivered > 0 + AND sm.date > %(start_date)s + AND rel.profile_id = %(profile_id)s + GROUP BY so.warehouse_id, sol.product_id + ORDER BY number_of_so_lines DESC + """, + { + "start_date": date, + "current_warehouse_id": warehouse.id, + "profile_id": self.id, + }, + ) + + result = self.env.cr.fetchall() + total = 0 + for r in result: + product_data = { + "product": self.env["product.product"].browse(r[0]), + "warehouse": self.env["stock.warehouse"].browse(r[1]), + "number_of_so_lines": int(r[2]), + } + total += int(r[2]) + product_list.append(product_data) + return product_list, total + + @api.model + def _compute_abc_classification(self): + def _get_sort_key_value(data): + return data["number_of_so_lines"] + + def _get_sort_key_percentage(rec): + return rec.percentage + + profiles = self.search([]).filtered(lambda p: p.level_ids) + + ProductClassification = self.env["abc.classification.product.level"] + + for profile in profiles: + start_date = fields.Datetime.to_string( + datetime.today() - timedelta(days=profile.period) + ) + + product_list, total = profile._fill_initial_product_data(start_date) + + levels = profile.level_ids.sorted( + key=_get_sort_key_percentage, reverse=True + ) + percentages = levels.mapped("percentage") + cum_percentages = [] + previous_percentage = None + for i, perc in enumerate(percentages): + if i == 0: + percentage_to_append = perc + cum_percentages.append(percentage_to_append) + else: + percentage_to_append = previous_percentage + perc + cum_percentages.append(percentage_to_append) + previous_percentage = percentage_to_append + + level_percentage = list(zip(levels, cum_percentages)) + + level, percentage = level_percentage.pop(0) + previous_data = {} + for i, product_data in enumerate(product_list): + + # Compute percentages and cumulative percentages for the products + product_data["number_of_so_lines_percentage"] = ( + (100.0 * product_data["number_of_so_lines"] / total) + if total + else 0.0 + ) + + product_data["cumulative_percentage"] = ( + product_data["number_of_so_lines_percentage"] + if i == 0 + else ( + product_data["number_of_so_lines_percentage"] + + previous_data["cumulative_percentage"] + ) + ) + if product_data["cumulative_percentage"] > 100: + raise UserError(_("Cumulative percentage greater than 100.")) + + # Compute ABC classification for the products based on the cumulative percentage + + if ( + product_data["cumulative_percentage"] > percentage + and len(level_percentage) > 0 + ): + level, percentage = level_percentage.pop(0) + + product_abc_classification = product_data[ + "product" + ].abc_product_classification_level_ids.filtered( + lambda p, profile: p.profile_id == profile.id + ) + + if product_abc_classification: + product_abc_classification.write({"computed_level_id": level.id}) + else: + product_abc_classification = ProductClassification.create( + { + "product_id": product_data["product"].id, + "profile_id": profile.id, + "computed_level_id": level.id, + } + ) + + previous_data = product_data diff --git a/product_abc_classification_sale_stock/models/product_template.py b/product_abc_classification_sale_stock/models/product_template.py new file mode 100644 index 000000000000..137ab8832aa9 --- /dev/null +++ b/product_abc_classification_sale_stock/models/product_template.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class ProductTemplate(models.Model): + + _inherit = "product.template" diff --git a/product_abc_classification_sale_stock/static/description/icon.png b/product_abc_classification_sale_stock/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_sale_stock/tests/__init__.py b/product_abc_classification_sale_stock/tests/__init__.py new file mode 100644 index 000000000000..bc83b0a6056a --- /dev/null +++ b/product_abc_classification_sale_stock/tests/__init__.py @@ -0,0 +1 @@ +from . import test_abc_classification_profile diff --git a/product_abc_classification_sale_stock/tests/test_abc_classification_profile.py b/product_abc_classification_sale_stock/tests/test_abc_classification_profile.py new file mode 100644 index 000000000000..92ed630bae63 --- /dev/null +++ b/product_abc_classification_sale_stock/tests/test_abc_classification_profile.py @@ -0,0 +1,273 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from freezegun import freeze_time + +from odoo.tests.common import SavepointCase + + +class TestAbcClassificationProfile(SavepointCase): + @classmethod + def setUpClass(cls): + super(TestAbcClassificationProfile, cls).setUpClass() + + cls.partner = cls.env["res.partner"].create( + {"name": "Unittest partner", "ref": "12344566777878"} + ) + + cls.warehouse_1 = cls.env.ref("stock.warehouse0") + cls.warehouse_1.write( + { + "name": "Test Warehouse", + "reception_steps": "one_step", + "delivery_steps": "pick_ship", + "code": "TST", + } + ) + cls.warehouse_1.pick_type_id.subcode = "PICK" + cls.warehouse_1.pick_type_id.groupbypartner = False + cls.warehouse_1.out_type_id.groupbypartner = True + + cls.level_A = cls.env["abc.classification.level"].create( + {"name": "A", "percentage": 80} + ) + + cls.level_B = cls.env["abc.classification.level"].create( + {"name": "B", "percentage": 15} + ) + + cls.level_C = cls.env["abc.classification.level"].create( + {"name": "C", "percentage": 5} + ) + + cls.stock_profile = cls.env["abc.classification.profile"].create( + { + "name": "Stock profile", + "profile_type": "stock", + "period": 365, + "level_ids": [(6, 0, [cls.level_A.id, cls.level_B.id, cls.level_C.id])], + } + ) + + cls.product1 = cls.env["product.product"].create( + { + "name": "Product1", + "uom_id": cls.env.ref("product.product_uom_unit").id, + "type": "product", + "default_code": "987654321", + "tracking": "none", + "abc_classification_profile_ids": [(4, cls.stock_profile.id)], + } + ) + + cls.product2 = cls.env["product.product"].create( + { + "name": "Product2", + "uom_id": cls.env.ref("product.product_uom_unit").id, + "type": "product", + "default_code": "123456789", + "tracking": "none", + "abc_classification_profile_ids": [(4, cls.stock_profile.id)], + } + ) + + cls.product3 = cls.env["product.product"].create( + { + "name": "Product3", + "uom_id": cls.env.ref("product.product_uom_unit").id, + "type": "product", + "default_code": "67548309", + "tracking": "none", + "abc_classification_profile_ids": [(4, cls.stock_profile.id)], + } + ) + + cls.product4 = cls.env["product.product"].create( + { + "name": "Product4", + "uom_id": cls.env.ref("product.product_uom_unit").id, + "type": "product", + "default_code": "123409876", + "tracking": "none", + "abc_classification_profile_ids": [(4, cls.stock_profile.id)], + } + ) + + cls.product5 = cls.env["product.product"].create( + { + "name": "Product5", + "uom_id": cls.env.ref("product.product_uom_unit").id, + "type": "product", + "default_code": "0987540321", + "tracking": "none", + "abc_classification_profile_ids": [(4, cls.stock_profile.id)], + } + ) + + cls.product6 = cls.env["product.product"].create( + { + "name": "Product6", + "uom_id": cls.env.ref("product.product_uom_unit").id, + "type": "product", + "default_code": "345789732", + "tracking": "none", + "abc_classification_profile_ids": [(4, cls.stock_profile.id)], + } + ) + + cls._create_availability(cls.product1) + cls._create_availability(cls.product2) + cls._create_availability(cls.product3) + cls._create_availability(cls.product4) + cls._create_availability(cls.product5) + cls._create_availability(cls.product6) + + cls.so1 = cls._confirm_sale_order( + products=[cls.product1, cls.product2, cls.product3], + qty={cls.product1.name: 80, cls.product2.name: 10, cls.product3.name: 30}, + ) + cls._confirm_pick_ship(cls.so1) + + cls.so2 = cls._confirm_sale_order( + products=[cls.product4, cls.product5, cls.product6], + qty={cls.product4.name: 5, cls.product5.name: 30, cls.product6.name: 25}, + ) + cls._confirm_pick_ship(cls.so2) + + cls.so3 = cls._confirm_sale_order( + products=[cls.product1], qty={cls.product1.name: 75} + ) + cls._confirm_pick_ship(cls.so3) + + cls.so3 = cls._confirm_sale_order( + products=[cls.product1], qty={cls.product1.name: 75} + ) + cls._confirm_pick_ship(cls.so3) + + cls.so4 = cls._confirm_sale_order( + products=[cls.product1], qty={cls.product1.name: 25} + ) + cls._confirm_pick_ship(cls.so4) + + cls.so5 = cls._confirm_sale_order( + products=[cls.product3, cls.product5], + qty={cls.product3.name: 90, cls.product5.name: 50}, + ) + cls._confirm_pick_ship(cls.so5) + + cls.so6 = cls._confirm_sale_order( + products=[cls.product6], qty={cls.product6.name: 30} + ) + cls._confirm_pick_ship(cls.so6) + + @classmethod + def _create_availability(cls, product): + update_qty_wizard = cls.env["stock.change.product.qty"].create( + { + "product_id": product.id, + "product_tmpl_id": product.product_tmpl_id.id, + "new_quantity": 500, + "location_id": cls.warehouse_1.lot_stock_id.id, + } + ) + update_qty_wizard.change_product_qty() + + @classmethod + def _confirm_sale_order(cls, products, qty, partner=None): + if partner is None: + partner = cls.partner + warehouse = cls.warehouse_1 + Sale = cls.env["sale.order"] + lines = [ + ( + 0, + 0, + { + "name": p.name, + "product_id": p.id, + "product_uom_qty": qty[p.name], + "product_uom": p.uom_id.id, + "price_unit": 1, + }, + ) + for p in products + ] + so_values = { + "partner_id": partner.id, + "warehouse_id": warehouse.id, + "order_line": lines, + } + so = Sale.create(so_values) + so.action_confirm() + return so + + @classmethod + def _confirm_pick_ship(cls, so): + pick = so.mapped("picking_ids").filtered( + lambda p: p.picking_type_subcode == "PICK" + ) + pick.action_confirm() + pick.action_assign() + for pack_op in pick.pack_operation_ids: + pack_op.qty_done = pack_op.product_qty + pick.action_done() + ship = so.mapped("picking_ids").filtered( + lambda p: p.picking_type_code == "outgoing" + ) + ship.action_confirm() + ship.action_assign() + for pack_op in ship.pack_operation_ids: + pack_op.qty_done = pack_op.product_qty + ship.action_done() + + @freeze_time("2021-01-01 07:10:00") + def test_00(self): + self.stock_profile._compute_abc_classification() + product_classification1 = self.env["abc.classification.product.level"].search( + [ + ("profile_id", "=", self.stock_profile.id), + ("product_id", "=", self.product1.id), + ] + ) + product_classification2 = self.env["abc.classification.product.level"].search( + [ + ("profile_id", "=", self.stock_profile.id), + ("product_id", "=", self.product2.id), + ] + ) + + product_classification3 = self.env["abc.classification.product.level"].search( + [ + ("profile_id", "=", self.stock_profile.id), + ("product_id", "=", self.product3.id), + ] + ) + + product_classification4 = self.env["abc.classification.product.level"].search( + [ + ("profile_id", "=", self.stock_profile.id), + ("product_id", "=", self.product4.id), + ] + ) + + product_classification5 = self.env["abc.classification.product.level"].search( + [ + ("profile_id", "=", self.stock_profile.id), + ("product_id", "=", self.product5.id), + ] + ) + + product_classification6 = self.env["abc.classification.product.level"].search( + [ + ("profile_id", "=", self.stock_profile.id), + ("product_id", "=", self.product6.id), + ] + ) + + self.assertEqual(product_classification1.computed_level_id.name, "A") + self.assertEqual(product_classification3.computed_level_id.name, "A") + self.assertEqual(product_classification5.computed_level_id.name, "A") + self.assertEqual(product_classification2.computed_level_id.name, "B") + self.assertEqual(product_classification6.computed_level_id.name, "B") + self.assertEqual(product_classification4.computed_level_id.name, "C") From 8d6e1509fe29f8f3fc4567becc62db886b643125 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Tue, 26 Jan 2021 15:30:41 +0100 Subject: [PATCH 03/21] [IMP] product_abc_classification: Add sale_stock classification profile This new profile provides a computation method based on the number of sale order line delivered by products --- product_abc_classification_base/README.rst | 101 ++++ .../__manifest__.py | 14 +- .../data/ir_cron.xml | 11 +- product_abc_classification_base/i18n/fr.po | 281 +++++++++++ .../models/__init__.py | 2 +- .../models/abc_classification_level.py | 25 +- .../abc_classification_product_level.py | 118 ++++- .../models/abc_classification_profile.py | 49 +- .../models/product_category.py | 26 -- .../models/product_product.py | 5 +- .../models/product_template.py | 63 ++- .../readme/CONTRIBUTORS.rst | 2 + .../readme/DESCRIPTION.rst | 11 +- .../security/ir.model.access.csv | 8 +- .../static/description/index.html | 442 ++++++++++++++++++ .../tests/__init__.py | 3 + .../tests/common.py | 92 ++++ .../test_abc_classification_product_level.py | 173 +++++++ .../tests/test_abc_classification_profile.py | 172 +++++++ .../tests/test_product.py | 115 +++++ .../abc_classification_product_level.xml | 77 +++ .../views/abc_classification_profile.xml | 24 +- .../views/product_product.xml | 18 + .../views/product_template.xml | 25 +- .../README.rst | 118 +++-- .../__manifest__.py | 18 +- .../demo/abc_classification_level.xml | 17 + .../demo/abc_classification_profile.xml | 12 + .../i18n/fr.po | 43 ++ .../models/__init__.py | 2 - .../models/abc_classification_level.py | 10 - .../models/abc_classification_profile.py | 268 ++++++++--- .../models/product_template.py | 10 - .../readme/CONTRIBUTORS.rst | 2 + .../readme/DESCRIPTION.rst | 2 + .../readme/USAGE.rst | 8 + .../static/description/index.html | 431 +++++++++++++++++ .../tests/test_abc_classification_profile.py | 176 ++++--- .../views/abc_classification_profile.xml | 15 + requirements.txt | 2 + .../.eggs/README.txt | 6 + .../odoo/__init__.py | 1 + .../odoo/addons/__init__.py | 1 + .../addons/product_abc_classification_base | 1 + .../product_abc_classification_base/setup.py | 6 + .../.eggs/README.txt | 6 + .../odoo/__init__.py | 1 + .../odoo/addons/__init__.py | 1 + .../product_abc_classification_sale_stock | 1 + .../setup.py | 6 + 50 files changed, 2658 insertions(+), 363 deletions(-) create mode 100644 product_abc_classification_base/README.rst create mode 100644 product_abc_classification_base/i18n/fr.po delete mode 100644 product_abc_classification_base/models/product_category.py create mode 100644 product_abc_classification_base/static/description/index.html create mode 100644 product_abc_classification_base/tests/__init__.py create mode 100644 product_abc_classification_base/tests/common.py create mode 100644 product_abc_classification_base/tests/test_abc_classification_product_level.py create mode 100644 product_abc_classification_base/tests/test_abc_classification_profile.py create mode 100644 product_abc_classification_base/tests/test_product.py create mode 100644 product_abc_classification_base/views/abc_classification_product_level.xml create mode 100644 product_abc_classification_base/views/product_product.xml create mode 100644 product_abc_classification_sale_stock/demo/abc_classification_level.xml create mode 100644 product_abc_classification_sale_stock/demo/abc_classification_profile.xml create mode 100644 product_abc_classification_sale_stock/i18n/fr.po delete mode 100644 product_abc_classification_sale_stock/models/abc_classification_level.py delete mode 100644 product_abc_classification_sale_stock/models/product_template.py create mode 100644 product_abc_classification_sale_stock/readme/CONTRIBUTORS.rst create mode 100644 product_abc_classification_sale_stock/readme/DESCRIPTION.rst create mode 100644 product_abc_classification_sale_stock/readme/USAGE.rst create mode 100644 product_abc_classification_sale_stock/static/description/index.html create mode 100644 product_abc_classification_sale_stock/views/abc_classification_profile.xml create mode 100644 setup/product_abc_classification_base/.eggs/README.txt create mode 100644 setup/product_abc_classification_base/odoo/__init__.py create mode 100644 setup/product_abc_classification_base/odoo/addons/__init__.py create mode 120000 setup/product_abc_classification_base/odoo/addons/product_abc_classification_base create mode 100644 setup/product_abc_classification_base/setup.py create mode 100644 setup/product_abc_classification_sale_stock/.eggs/README.txt create mode 100644 setup/product_abc_classification_sale_stock/odoo/__init__.py create mode 100644 setup/product_abc_classification_sale_stock/odoo/addons/__init__.py create mode 120000 setup/product_abc_classification_sale_stock/odoo/addons/product_abc_classification_sale_stock create mode 100644 setup/product_abc_classification_sale_stock/setup.py diff --git a/product_abc_classification_base/README.rst b/product_abc_classification_base/README.rst new file mode 100644 index 000000000000..196e2ed4364b --- /dev/null +++ b/product_abc_classification_base/README.rst @@ -0,0 +1,101 @@ +============================== +Alc 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/10.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-10-0/product-attribute-10-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/10.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 + +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_base/__manifest__.py b/product_abc_classification_base/__manifest__.py index 2b8ccf1ffee8..7ae04a9b7d73 100644 --- a/product_abc_classification_base/__manifest__.py +++ b/product_abc_classification_base/__manifest__.py @@ -1,19 +1,23 @@ # -*- coding: utf-8 -*- +# Copyright 2020 ForgeFlow # Copyright 2021 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { - "name": "Alc Product Abc Classification", + "name": "Product Abc Classification", "summary": """ ABC classification for sales and warehouse management""", "version": "10.0.1.0.0", "license": "AGPL-3", - "author": "ACSONE SA/NV,Odoo Community Association (OCA)", - "depends": ["product"], + "author": "ACSONE SA/NV, ForgeFlow, Odoo Community Association (OCA)", + "depends": ["product", "stock", "web_m2x_options"], "data": [ - "views/product_category.xml", - "views/product_template.xml", + "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", ], "demo": [], } diff --git a/product_abc_classification_base/data/ir_cron.xml b/product_abc_classification_base/data/ir_cron.xml index 36faa7971a5e..a8bb93fd5460 100644 --- a/product_abc_classification_base/data/ir_cron.xml +++ b/product_abc_classification_base/data/ir_cron.xml @@ -1,13 +1,14 @@ - + Perform the product ABC Classification + 1 - days + months -1 - - model._compute_abc_classification() - code + abc.classification.profile + _cron_compute_abc_classification + () diff --git a/product_abc_classification_base/i18n/fr.po b/product_abc_classification_base/i18n/fr.po new file mode 100644 index 000000000000..93eb0ee151a0 --- /dev/null +++ b/product_abc_classification_base/i18n/fr.po @@ -0,0 +1,281 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_abc_classification_base +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-02-03 06:57+0000\n" +"PO-Revision-Date: 2021-02-03 06:57+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_base +#: model:ir.ui.view,arch_db:product_abc_classification_base.product_template_form_view +msgid "ABC Classification" +msgstr "Classification ABC" + +#. module: product_abc_classification_base +#: model:ir.ui.view,arch_db:product_abc_classification_base.abc_classification_product_level_form_view +#: model:ir.model,name:product_abc_classification_base.model_abc_classification_product_level +msgid "ABC Classification Product Level" +msgstr "Niveau de classification ABC des articles" + +#. module: product_abc_classification_base +#: model:ir.actions.act_window,name:product_abc_classification_base.abc_classification_profile_action +#: model:ir.ui.menu,name:product_abc_classification_base.menu_abc_classification_profile_config_stock +msgid "ABC Classification profiles" +msgstr "Profils de classification ABC" + +#. module: product_abc_classification_base +#: model:ir.ui.view,arch_db:product_abc_classification_base.abc_classification_profile_form_view +msgid "ABC Profile" +msgstr "Profil ABC" + +#. module: product_abc_classification_base +#: model:ir.ui.view,arch_db:product_abc_classification_base.abc_classification_profile_tree_view +msgid "ABC Profiles" +msgstr "Profils ABC" + + +#. module: product_abc_classification_base +#: model:ir.model,name:product_abc_classification_base.model_abc_classification_profile +msgid "Abc Classification Profile" +msgstr "Profil de classification ABC" + +#. module: product_abc_classification_base +#: model:ir.ui.view,arch_db:product_abc_classification_base.abc_classification_product_level_search_view +msgid "Abc classification" +msgstr "Classification ABC" + +#. module: product_abc_classification_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_delivery_carrier_abc_classification_product_level_ids +#: model:ir.model.fields,field_description:product_abc_classification_base.field_product_product_abc_classification_product_level_ids +#: model:ir.model.fields,field_description:product_abc_classification_base.field_product_template_abc_classification_product_level_ids +msgid "Abc classification product level ids" +msgstr "Classes ABC" + +#. module: product_abc_classification_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_allowed_profile_ids +#: model:ir.model.fields,field_description:product_abc_classification_base.field_delivery_carrier_abc_classification_profile_ids +#: model:ir.model.fields,field_description:product_abc_classification_base.field_product_product_abc_classification_profile_ids +#: model:ir.model.fields,field_description:product_abc_classification_base.field_product_template_abc_classification_profile_ids +msgid "Abc classification profile ids" +msgstr "Profils ABC" + + +#. module: product_abc_classification_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_level_id +msgid "Classification level" +msgstr "Classe / Niveau" + +#. module: product_abc_classification_base +#: code:addons/product_abc_classification_base/models/abc_classification_product_level.py:75 +#, python-format +msgid "Classification level is mandatory" +msgstr "La classe / niveau est obligatoire" + +#. module: product_abc_classification_base +#: model:ir.ui.view,arch_db:product_abc_classification_base.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_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_computed_level_id +msgid "Computed classification level" +msgstr "Classe calculée" + +#. module: product_abc_classification_base +#: model:ir.ui.view,arch_db:product_abc_classification_base.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_base +#: code:addons/product_abc_classification_base/models/abc_classification_product_level.py:81 +#, 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_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level_create_uid +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_create_uid +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_create_uid +msgid "Created by" +msgstr "Créé par" + +#. module: product_abc_classification_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level_create_date +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_create_date +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_create_date +msgid "Created on" +msgstr "Créé le" + +#. module: product_abc_classification_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level_display_name +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_display_name +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: product_abc_classification_base +#: model:ir.ui.view,arch_db:product_abc_classification_base.abc_classification_product_level_search_view +msgid "Group By" +msgstr "Grouper par" + +#. module: product_abc_classification_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level_id +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_id +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_id +msgid "ID" +msgstr "ID" + +#. module: product_abc_classification_base +#: model:ir.model.fields,field_description:product_abc_classification_base.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_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level___last_update +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level___last_update +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile___last_update +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: product_abc_classification_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level_write_uid +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_write_uid +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_write_uid +msgid "Last Updated by" +msgstr "Dernière mise à jour par" + +#. module: product_abc_classification_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level_write_date +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_write_date +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_write_date +msgid "Last Updated on" +msgstr "Dernière mise à jour le" + +#. module: product_abc_classification_base +#: model:ir.ui.view,arch_db:product_abc_classification_base.abc_classification_product_level_search_view +msgid "Level" +msgstr "Classe" + +#. module: product_abc_classification_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_level_ids +msgid "Level ids" +msgstr "Classes" + +#. module: product_abc_classification_base +#: sql_constraint:abc.classification.level:0 +#: code:addons/product_abc_classification_base/models/abc_classification_level.py:26 +#, 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_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_manual_level_id +msgid "Manual classification level" +msgstr "Classe (Valeur à utiliser)" + +#. module: product_abc_classification_base +#: code:addons/product_abc_classification_base/models/abc_classification_product_level.py:91 +#, 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_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level_name +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_name +msgid "Name" +msgstr "Nom" + +#. module: product_abc_classification_base +#: sql_constraint:abc.classification.product.level:0 +#: code:addons/product_abc_classification_base/models/abc_classification_product_level.py:67 +#, 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_base +#: model:ir.model.fields,field_description:product_abc_classification_base.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_base +#: model:ir.model,name:product_abc_classification_base.model_product_product +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_product_id +msgid "Product" +msgstr "Article" + +#. module: product_abc_classification_base +#: model:ir.model,name:product_abc_classification_base.model_product_template +msgid "Product Template" +msgstr "Modèle de produit" + +#. module: product_abc_classification_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_product_tmpl_id +msgid "Product template" +msgstr "Modèle de produit" + +#. module: product_abc_classification_base +#: model:ir.actions.act_window,name:product_abc_classification_base.abc_classification_product_level_action +#: model:ir.ui.menu,name:product_abc_classification_base.menu_abc_classification_product_level_config_stock +msgid "Products ABC Classification" +msgstr "Classification ABC des articles" + +#. module: product_abc_classification_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_profile_id +#: model:ir.ui.view,arch_db:product_abc_classification_base.abc_classification_product_level_search_view +msgid "Profile" +msgstr "Profil" + +#. module: product_abc_classification_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level_profile_id +msgid "Profile id" +msgstr "Profil" + +#. module: product_abc_classification_base +#: sql_constraint:abc.classification.profile:0 +#: code:addons/product_abc_classification_base/models/abc_classification_profile.py:32 +#, python-format +msgid "Profile name must be unique" +msgstr "Le nom du profil doit être unique" + +#. module: product_abc_classification_base +#: code:addons/product_abc_classification_base/models/abc_classification_level.py:35 +#, python-format +msgid "The percentage cannot be greater than 100." +msgstr "Le pourcentage ne peut pas dépasser 100." + +#. module: product_abc_classification_base +#: code:addons/product_abc_classification_base/models/abc_classification_level.py:39 +#, python-format +msgid "The percentage should be a positive number." +msgstr "Le pourcentage doit être un nombre positif." + +#. module: product_abc_classification_base +#: code:addons/product_abc_classification_base/models/abc_classification_profile.py:51 +#, 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_base +#: code:addons/product_abc_classification_base/models/abc_classification_profile.py:42 +#, python-format +msgid "The sum of the percentages of the levels should be 100." +msgstr "La somme des pourcentage ne doit pas dépasser 100." + +#. module: product_abc_classification_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_profile_type +msgid "Type of ABC classification" +msgstr "Type de classification ABC" + +#. module: product_abc_classification_base +#: model:ir.model,name:product_abc_classification_base.model_abc_classification_level +msgid "abc.classification.level" +msgstr "Classe de classification ABC" diff --git a/product_abc_classification_base/models/__init__.py b/product_abc_classification_base/models/__init__.py index 5fa33c2bb220..b98adc64bd49 100644 --- a/product_abc_classification_base/models/__init__.py +++ b/product_abc_classification_base/models/__init__.py @@ -2,4 +2,4 @@ from . import abc_classification_level from . import product_template from . import product_product -from . import abc_product_classification_level +from . import abc_classification_product_level diff --git a/product_abc_classification_base/models/abc_classification_level.py b/product_abc_classification_base/models/abc_classification_level.py index c83a32271160..85bd7781a6d8 100644 --- a/product_abc_classification_base/models/abc_classification_level.py +++ b/product_abc_classification_base/models/abc_classification_level.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# Copyright 2020 ForgeFlow # Copyright 2021 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -10,17 +11,31 @@ class AbcClassificationLevel(models.Model): _name = "abc.classification.level" _order = "percentage desc, id desc" + _rec_name = "name" percentage = fields.Float(default=0.0, required=True, string="%") - profile_id = fields.Many2one("abc.classification.profile") + profile_id = fields.Many2one( + "abc.classification.profile", ondelete="cascade" + ) - name = fields.Char(help="Classification A, B or C") # unique par profile + 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.")) + raise ValidationError( + _("The percentage cannot be greater than 100.") + ) if level.percentage <= 0.0: - raise ValidationError(_("The percentage should be a positive number.")) - + raise ValidationError( + _("The percentage should be a positive number.") + ) diff --git a/product_abc_classification_base/models/abc_classification_product_level.py b/product_abc_classification_base/models/abc_classification_product_level.py index 462c6637a29f..f795051bf884 100644 --- a/product_abc_classification_base/models/abc_classification_product_level.py +++ b/product_abc_classification_base/models/abc_classification_product_level.py @@ -2,35 +2,117 @@ # 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 import api, fields, models, _ +from odoo.exceptions import ValidationError -class AbcProductClassificationLevel(models.Model): +class AbcClassificationProductLevel(models.Model): + _name = "abc.classification.product.level" + _inherit = "mail.thread" + _description = "Abc Classification Product Level" + _rec_name = "level_id" - _name = "abc.product.classification.level" - _description = "Abc Product Classification Level" - - name = fields.Char() + display_name = fields.Char(compute="_compute_display_name") manual_level_id = fields.Many2one( - "abc.classification.level", string="Manual classification level" + "abc.classification.level", + string="Manual classification level", + track_visibility="onchange", + domain="[('profile_id', '=', profile_id)]" ) computed_level_id = fields.Many2one( - "abc.classification.level", string="Computed classification level" + "abc.classification.level", + string="Computed classification level", + track_visibility="onchange", + 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", + 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, ) - product_id = fields.Many2one("product.product", string="Product", index=True) # percentage - profile_id = fields.Many2one("abc.classification.profile", string="Profile") + profile_id = fields.Many2one( + "abc.classification.profile", + string="Profile", + required=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 = u"{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): @@ -43,5 +125,15 @@ def _compute_level_id(self): @api.depends("manual_level_id", "computed_level_id") def _compute_flag(self): for rec in self: - if rec.manual_level_id != rec.computed_level_id: - rec.flag = True + rec.flag = ( + rec.computed_level_id + and rec.manual_level_id != rec.computed_level_id + ) + + @api.model + def create(self, vals): + 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"] + return super(AbcClassificationProductLevel, self).create(vals) diff --git a/product_abc_classification_base/models/abc_classification_profile.py b/product_abc_classification_base/models/abc_classification_profile.py index fa3c3cdc8b90..552aaa7a0284 100644 --- a/product_abc_classification_base/models/abc_classification_profile.py +++ b/product_abc_classification_base/models/abc_classification_profile.py @@ -1,23 +1,27 @@ # -*- coding: utf-8 -*- +# 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 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() - level_ids = fields.Many2many( + name = fields.Char(required=True) + level_ids = fields.One2many( comodel_name="abc.classification.level", inverse_name="profile_id" ) - display_name = fields.Char() - profile_type = fields.Selection( - selection=[], string="Type of ABC classification", index=True, required=True + selection=[], + string="Type of ABC classification", + index=True, + required=True, ) period = fields.Integer( default=365, @@ -25,12 +29,33 @@ class AbcClassificationProfile(models.Model): required=True, ) - def _fill_initial_product_data(self, date): - raise NotImplementedError() - - def _fill_data_(self, date, product_list): + _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.") + ) + + @api.multi + def _compute_abc_classification(self): raise NotImplementedError() @api.model - def _compute_abc_classification(self): - raise NotImplementedError() + def _cron_compute_abc_classification(self): + self.search([])._compute_abc_classification() diff --git a/product_abc_classification_base/models/product_category.py b/product_abc_classification_base/models/product_category.py deleted file mode 100644 index df7348e554cb..000000000000 --- a/product_abc_classification_base/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_base/models/product_product.py b/product_abc_classification_base/models/product_product.py index 2632c1d6fad7..ad0c441ab5c5 100644 --- a/product_abc_classification_base/models/product_product.py +++ b/product_abc_classification_base/models/product_product.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# Copyright 2020 ForgeFlow # Copyright 2021 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -9,8 +10,8 @@ class ProductProduct(models.Model): _inherit = "product.product" - abc_product_classification_level_ids = fields.Many2many( - "abc.product.classification.level", index=True + 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", diff --git a/product_abc_classification_base/models/product_template.py b/product_abc_classification_base/models/product_template.py index 952cb26f6879..6eff21320142 100644 --- a/product_abc_classification_base/models/product_template.py +++ b/product_abc_classification_base/models/product_template.py @@ -1,17 +1,68 @@ # -*- coding: utf-8 -*- +# 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 +from odoo import api, fields, models class ProductTemplate(models.Model): - _inherit = "product.template" - abc_product_classification_level_ids = fields.Many2many( - related="product_variant_ids.abc_product_classification_level_ids" - ) abc_classification_profile_ids = fields.Many2many( - related="product_variant_ids.abc_classification_profile_ids" + "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_base/readme/CONTRIBUTORS.rst b/product_abc_classification_base/readme/CONTRIBUTORS.rst index 2e34e218a547..ebb18f6ff748 100644 --- a/product_abc_classification_base/readme/CONTRIBUTORS.rst +++ b/product_abc_classification_base/readme/CONTRIBUTORS.rst @@ -1 +1,3 @@ * Miquel Raïch +* Lindsay Marion +* Laurent Mignon diff --git a/product_abc_classification_base/readme/DESCRIPTION.rst b/product_abc_classification_base/readme/DESCRIPTION.rst index b55e108765fd..e8bc6d5b704d 100644 --- a/product_abc_classification_base/readme/DESCRIPTION.rst +++ b/product_abc_classification_base/readme/DESCRIPTION.rst @@ -1,8 +1,11 @@ -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. +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_base/security/ir.model.access.csv b/product_abc_classification_base/security/ir.model.access.csv index 421beb328a90..9283b3539606 100644 --- a/product_abc_classification_base/security/ir.model.access.csv +++ b/product_abc_classification_base/security/ir.model.access.csv @@ -1,5 +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,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 +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_base/static/description/index.html b/product_abc_classification_base/static/description/index.html new file mode 100644 index 000000000000..dd0266cbce4e --- /dev/null +++ b/product_abc_classification_base/static/description/index.html @@ -0,0 +1,442 @@ + + + + + + +Alc Product Abc Classification + + + +
+

Alc 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_base/tests/__init__.py b/product_abc_classification_base/tests/__init__.py new file mode 100644 index 000000000000..8292c06ca325 --- /dev/null +++ b/product_abc_classification_base/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_base/tests/common.py b/product_abc_classification_base/tests/common.py new file mode 100644 index 000000000000..fc7645a34c87 --- /dev/null +++ b/product_abc_classification_base/tests/common.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests.common import SavepointCase + + +class ABCClassificationCase(SavepointCase): + @classmethod + def setUpClass(cls): + super(ABCClassificationCase, cls).setUpClass() + # 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(ABCClassificationLevelCase, cls).setUpClass() + cls.classification_profile.write( + { + "level_ids": [ + (0, 0, {"percentage": 60, "name": "a"}), + (0, 0, {"percentage": 40, "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, "name": "a"}), + (0, 0, {"percentage": 20, "name": "b"}), + ], + } + ) + levels = cls.classification_profile_bis.level_ids + cls.classification_level_bis_a = levels.filtered( + lambda l: l.name == "a" + ) + + # 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": False, + "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("product.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, + "attribute_value_ids": [(6, 0, size_value.ids)], + } + ) diff --git a/product_abc_classification_base/tests/test_abc_classification_product_level.py b/product_abc_classification_base/tests/test_abc_classification_product_level.py new file mode 100644 index 000000000000..e3babcecbe1b --- /dev/null +++ b/product_abc_classification_base/tests/test_abc_classification_product_level.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from psycopg2 import IntegrityError + +from .common import ABCClassificationLevelCase +from odoo.exceptions import ValidationError + + +class TestABCClassificationProductLevel(ABCClassificationLevelCase): + @classmethod + def setUpClass(cls): + super(TestABCClassificationProductLevel, cls).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 + } + ) + + 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 + } + ) diff --git a/product_abc_classification_base/tests/test_abc_classification_profile.py b/product_abc_classification_base/tests/test_abc_classification_profile.py new file mode 100644 index 000000000000..6538946862a1 --- /dev/null +++ b/product_abc_classification_base/tests/test_abc_classification_profile.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from psycopg2 import IntegrityError + +from .common import ABCClassificationCase +from odoo.exceptions import ValidationError + + +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, "name": "A"}), + (0, 0, {"percentage": 40, "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, "name": "A"}), + (0, 0, {"percentage": 30, "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, "name": "A"}), + (0, 0, {"percentage": 50, "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, "name": "A"}), + (0, 0, {"percentage": 50, "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, "name": "A"}), + (0, 0, {"percentage": -50, "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, "name": "A"}), + (0, 0, {"percentage": 40, "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, "name": "A"}), + (0, 0, {"percentage": 40, "name": "B"}), + ] + } + ) + new_profile = self.ABCClassificationProfile.create( + { + "name": "New Profile test", + "profile_type": "test_type", + "level_ids": [ + (0, 0, {"percentage": 60, "name": "A"}), + (0, 0, {"percentage": 40, "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_base/tests/test_product.py b/product_abc_classification_base/tests/test_product.py new file mode 100644 index 000000000000..706c86d1d215 --- /dev/null +++ b/product_abc_classification_base/tests/test_product.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +# 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(TestProduct, cls).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_base/views/abc_classification_product_level.xml b/product_abc_classification_base/views/abc_classification_product_level.xml new file mode 100644 index 000000000000..e4c7e0486fc0 --- /dev/null +++ b/product_abc_classification_base/views/abc_classification_product_level.xml @@ -0,0 +1,77 @@ + + + + + abc.classification.product.level.form (in product_abc_classification_base) + abc.classification.product.level + +
+ + + + + + + + + + + + + + +
+ + +
+
+
+
+ + abc.classification.product.level.tree (in product_abc_classification_base) + abc.classification.product.level + + + + + + + + + + + + abc.classification.product.level.search (in product_abc_classification_base) + 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_base/views/abc_classification_profile.xml b/product_abc_classification_base/views/abc_classification_profile.xml index a6c22d0689ff..cff772fc1d79 100644 --- a/product_abc_classification_base/views/abc_classification_profile.xml +++ b/product_abc_classification_base/views/abc_classification_profile.xml @@ -3,7 +3,7 @@ License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). --> - abc.classification.profile.form + abc.classification.profile.form (in product_abc_classification_base) abc.classification.profile
@@ -12,8 +12,8 @@ - - + + @@ -26,7 +26,7 @@ - abc.classification.profile.tree + abc.classification.profile.tree (in product_abc_classification_base) abc.classification.profile @@ -35,17 +35,9 @@ - ABC Classification Profile + ABC Classification profiles abc.classification.profile tree,form - -

- Click to add a new profile. -

-

- This allows to create an ABC classification. -

-
- diff --git a/product_abc_classification_base/views/product_product.xml b/product_abc_classification_base/views/product_product.xml new file mode 100644 index 000000000000..8f8aa162366f --- /dev/null +++ b/product_abc_classification_base/views/product_product.xml @@ -0,0 +1,18 @@ + + + + + product.product.form (ABC Classification) + product.product + + + + {'default_product_id': active_id, 'default_profile_id': 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_base/views/product_template.xml b/product_abc_classification_base/views/product_template.xml index 6ae2aa3c49c8..77f15cf33355 100644 --- a/product_abc_classification_base/views/product_template.xml +++ b/product_abc_classification_base/views/product_template.xml @@ -2,35 +2,24 @@ - - product.template.tree - product.template - - - - - - - product.template.form (ABC Classification) product.template - + - + diff --git a/product_abc_classification_sale_stock/README.rst b/product_abc_classification_sale_stock/README.rst index 2e60ba3a5a43..461907d96c63 100644 --- a/product_abc_classification_sale_stock/README.rst +++ b/product_abc_classification_sale_stock/README.rst @@ -1,91 +1,87 @@ -.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html - :alt: License: AGPL-3 - -======================================== -Alc Product Abc Classification Warehouse -======================================== - -Get ABC classification for warehouse type - -Installation -============ - -To install this module, you need to: - -#. Do this ... - -Configuration -============= - -To configure this module, you need to: - -#. Go to ... - -.. figure:: path/to/local/image.png - :alt: alternative description - :width: 600 px +====================================================== +Product Abc Classification based on delivered products +====================================================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! 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/10.0/product_abc_classification_sale_stock + :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-10-0/product-attribute-10-0-product_abc_classification_sale_stock + :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/10.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This modules includes an ABC analysis computation profile based +on the number of sale order lines delivered from a given date by product. + +**Table of contents** + +.. contents:: + :local: Usage ===== To use this module, you need to: -#. Go to ... - -.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas - :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/{repo_id}/{branch} +#. 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. -.. repo_id is available in https://github.com/OCA/maintainer-tools/blob/master/tools/repos_with_ids.txt -.. branch is "8.0" for example - -Known issues / Roadmap -====================== - -* ... +#. Later you should go to product variants, and assign them a profile. +Then the cron classification will proceed to assign to these products one of the profile's levels. 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 smash it by providing detailed and welcomed feedback. +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 ======= -Images ------- +Authors +~~~~~~~ -* Odoo Community Association: `Icon `_. +* ACSONE SA/NV Contributors ------------- - -* Firstname Lastname -* Second Person - -Funders -------- +~~~~~~~~~~~~ -The development of this module has been financially supported by: +* Lindsay Marion +* Laurent Mignon -* Company 1 name -* Company 2 name +Maintainers +~~~~~~~~~~~ -Maintainer ----------- +This module is maintained by the OCA. .. image:: https://odoo-community.org/logo.png :alt: Odoo Community Association :target: https://odoo-community.org -This module is maintained by the OCA. - 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. -To contribute to this module, please visit https://odoo-community.org. +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_sale_stock/__manifest__.py b/product_abc_classification_sale_stock/__manifest__.py index 88b690f8aade..c60f80bcd0a0 100644 --- a/product_abc_classification_sale_stock/__manifest__.py +++ b/product_abc_classification_sale_stock/__manifest__.py @@ -3,19 +3,17 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { - "name": "Alc Product Abc Classification Warehouse", + "name": "Product Abc Classification based on delivered products", "summary": """ - Get ABC classification for warehouse type""", + Compute ABC classification from the number of delivered sale order + line by product""", "version": "10.0.1.0.0", "license": "AGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", - "depends": [ - "alc_product_abc_classification", - "product", - "sale_stock", - "stock_picking_subcode", - "stock_delivery_note", + "depends": ["product_abc_classification_base", "sale_stock"], + "data": ["views/abc_classification_profile.xml"], + "demo": [ + "demo/abc_classification_level.xml", + "demo/abc_classification_profile.xml", ], - "data": [], - "demo": [], } diff --git a/product_abc_classification_sale_stock/demo/abc_classification_level.xml b/product_abc_classification_sale_stock/demo/abc_classification_level.xml new file mode 100644 index 000000000000..d1c93d1902a6 --- /dev/null +++ b/product_abc_classification_sale_stock/demo/abc_classification_level.xml @@ -0,0 +1,17 @@ + + + + + a + 80 + + + b + 15 + + + c + 5 + + diff --git a/product_abc_classification_sale_stock/demo/abc_classification_profile.xml b/product_abc_classification_sale_stock/demo/abc_classification_profile.xml new file mode 100644 index 000000000000..aee51147dcb0 --- /dev/null +++ b/product_abc_classification_sale_stock/demo/abc_classification_profile.xml @@ -0,0 +1,12 @@ + + + + + Sale stock profile + sale_stock + + 365 + + + diff --git a/product_abc_classification_sale_stock/i18n/fr.po b/product_abc_classification_sale_stock/i18n/fr.po new file mode 100644 index 000000000000..f9c51f29cdb8 --- /dev/null +++ b/product_abc_classification_sale_stock/i18n/fr.po @@ -0,0 +1,43 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_abc_classification_sale_stock +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-02-03 07:21+0000\n" +"PO-Revision-Date: 2021-02-03 07:21+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_sale_stock +#: 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_sale_stock +#: model:ir.model,name:product_abc_classification_sale_stock.model_abc_classification_profile +msgid "Abc Classification Profile" +msgstr "Profil de classification ABC" + +#. module: product_abc_classification_sale_stock +#: code:addons/product_abc_classification_sale_stock/models/abc_classification_profile.py:247 +#, python-format +msgid "Cumulative percentage greater than 100." +msgstr "Somme des pourcentages calculés supérieure à 100." + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_classification_profile_warehouse_id +msgid "Warehouse" +msgstr "Entrepôt" + +#. module: product_abc_classification_sale_stock +#: code:addons/product_abc_classification_sale_stock/models/abc_classification_profile.py:39 +#, python-format +msgid "You must specify a warehouse for {profile_name}" +msgstr "Vous devez specifier un entrepôt pour le profil {profile_name}" diff --git a/product_abc_classification_sale_stock/models/__init__.py b/product_abc_classification_sale_stock/models/__init__.py index 424bcbb81ccb..3737cdc08877 100644 --- a/product_abc_classification_sale_stock/models/__init__.py +++ b/product_abc_classification_sale_stock/models/__init__.py @@ -1,3 +1 @@ from . import abc_classification_profile -from . import abc_classification_level -from . import product_template diff --git a/product_abc_classification_sale_stock/models/abc_classification_level.py b/product_abc_classification_sale_stock/models/abc_classification_level.py deleted file mode 100644 index cd72ba44dda6..000000000000 --- a/product_abc_classification_sale_stock/models/abc_classification_level.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2021 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo import models - - -class AbcClassificationLevel(models.Model): - - _inherit = "abc.classification.level" diff --git a/product_abc_classification_sale_stock/models/abc_classification_profile.py b/product_abc_classification_sale_stock/models/abc_classification_profile.py index 7a51671928e7..47cce0687165 100644 --- a/product_abc_classification_sale_stock/models/abc_classification_profile.py +++ b/product_abc_classification_sale_stock/models/abc_classification_profile.py @@ -3,97 +3,238 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from datetime import datetime, timedelta +from operator import attrgetter from odoo import _, api, fields, models -from odoo.exceptions import UserError +from odoo.tools import float_round +from odoo.exceptions import UserError, ValidationError class AbcClassificationProfile(models.Model): _inherit = "abc.classification.profile" - profile_type = fields.Selection(selection_add=[("stock", "Stock")]) + profile_type = fields.Selection( + selection_add=[ + ( + "sale_stock", + "Based on the count of delivered sale order line by product", + ) + ] + ) + warehouse_id = fields.Many2one( + "stock.warehouse", + "Warehouse", + ondelete="cascade", + default=lambda self: self.env["stock.warehouse"].search( + [("company_id", "=", self.env.user.company_id.id)], limit=1 + ), + ) + + @api.constrains("profile_type", "warehouse_id") + def _check_warehouse_id(self): + for rec in self: + if rec.profile_type == "sale_stock" and not rec.warehouse_id: + raise ValidationError( + _( + "You must specify a warehouse for {profile_name}" + ).forman(profile_name=rec.name) + ) def _fill_initial_product_data(self, date): product_list = [] - if self.profile_type == "stock": + if self.profile_type == "sale_stock": return self._fill_data(date, product_list) return product_list, 0 - def _fill_data(self, date, product_list): + def _get_all_product_ids(self): + """Get a set of product ids with the current profile""" + self.ensure_one() + self.env.cr.execute( + """ + SELECT + product_id + FROM + abc_classification_profile_product_rel + JOIN + product_product pp + ON pp.id = product_id + WHERE + pp.active + AND profile_id = %(profile_id)s + """, + {"profile_id": self.id}, + ) + return {r[0] for r in self.env.cr.fetchall()} + + def _get_data(self, from_date=None): + """Get a list of statics info from the DB ordered by number of lines desc + """ self.ensure_one() - warehouse = self.env.ref("stock.warehouse0") + from_date = ( + from_date + if from_date + else fields.Datetime.to_string( + datetime.today() - timedelta(days=self.period) + ) + ) + customer_location_ids = ( + self.env["stock.location"].search([("usage", "=", "customer")]).ids + ) + # Collect all the product linked to the profile to be sure to provide + # information also for product no sold into the given period + all_product_ids = self._get_all_product_ids() + + # Count the number of delivered order line by product linked to a + # stock_move with a customer location as destination and a date later + # than the given date self.env.cr.execute( - """ SELECT sol.product_id product_id, so.warehouse_id warehouse_id, COUNT(sol.id) number_of_so_lines + """ SELECT + sol.product_id product_id, + COUNT(sol.id) number_of_so_lines FROM sale_order so JOIN - sale_order_line sol ON sol.order_id = so.id + sale_order_line sol ON + sol.order_id = so.id JOIN - stock_move sm ON sol.id = sm.order_line_id + abc_classification_profile_product_rel rel + ON rel.product_id = sol.product_id JOIN - abc_classification_profile_product_rel rel ON rel.product_id = sol.product_id + product_product pp + ON pp.id = sol.product_id WHERE sol.qty_delivered > 0 - AND sm.date > %(start_date)s + AND pp.active AND rel.profile_id = %(profile_id)s - GROUP BY so.warehouse_id, sol.product_id + AND so.warehouse_id = %(current_warehouse_id)s + AND EXISTS ( + SELECT + 1 + FROM + stock_move sm + JOIN + procurement_order po + on po.id = sm.procurement_id + WHERE + sm.date > %(start_date)s + AND sm.location_dest_id in %(customer_loc_ids)s + AND po.sale_line_id = sol.id + ) + + GROUP BY sol.product_id ORDER BY number_of_so_lines DESC """, { - "start_date": date, - "current_warehouse_id": warehouse.id, + "start_date": from_date, + "current_warehouse_id": self.warehouse_id.id, "profile_id": self.id, + "customer_loc_ids": tuple(customer_location_ids), }, ) result = self.env.cr.fetchall() + total = 0 + product_list = [] for r in result: + product_id = r[0] product_data = { - "product": self.env["product.product"].browse(r[0]), - "warehouse": self.env["stock.warehouse"].browse(r[1]), - "number_of_so_lines": int(r[2]), + "product": self.env["product.product"].browse(product_id), + "number_of_so_lines": int(r[1]), } - total += int(r[2]) + total += int(r[1]) product_list.append(product_data) + all_product_ids.remove(product_id) + # Add all products not sold or not delivered into this timelapse + for product_id in all_product_ids: + product_list.append( + { + "product": self.env["product.product"].browse(product_id), + "number_of_so_lines": 0, + } + ) return product_list, total - @api.model - def _compute_abc_classification(self): - def _get_sort_key_value(data): - return data["number_of_so_lines"] + def _build_ordered_level_cumulative_percentage(self): + """Return an ordered list of tuple of level, cumulative percentage - def _get_sort_key_percentage(rec): - return rec.percentage + The ordering is based on the level with the higher percentage first + """ + self.ensure_one() + levels = self.level_ids.sorted( + key=attrgetter("percentage"), reverse=True + ) + percentages = levels.mapped("percentage") + cum_percentages = [] + previous_percentage = None + for i, perc in enumerate(percentages): + if i == 0: + percentage_to_append = perc + cum_percentages.append(percentage_to_append) + else: + percentage_to_append = previous_percentage + perc + cum_percentages.append(percentage_to_append) + previous_percentage = percentage_to_append - profiles = self.search([]).filtered(lambda p: p.level_ids) + return list(zip(levels, cum_percentages)) - ProductClassification = self.env["abc.classification.product.level"] - - for profile in profiles: - start_date = fields.Datetime.to_string( - datetime.today() - timedelta(days=profile.period) - ) + def _get_existing_level_ids(self): + self.ensure_one() + self.env.cr.execute( + """ + SELECT + id + FROM + abc_classification_product_level + WHERE + profile_id = %(profile_id)s + """, + {"profile_id": self.id}, + ) + return {r[0] for r in self.env.cr.fetchall()} - product_list, total = profile._fill_initial_product_data(start_date) + def _purge_obsolete_level_values(self, ids_to_remove): + if not ids_to_remove: + return + self.env.cr.execute( + """ + DELETE FROM + abc_classification_product_level + WHERE + id in %(ids)s + """, + {"ids": tuple(ids_to_remove)}, + ) - levels = profile.level_ids.sorted( - key=_get_sort_key_percentage, reverse=True - ) - percentages = levels.mapped("percentage") - cum_percentages = [] - previous_percentage = None - for i, perc in enumerate(percentages): - if i == 0: - percentage_to_append = perc - cum_percentages.append(percentage_to_append) - else: - percentage_to_append = previous_percentage + perc - cum_percentages.append(percentage_to_append) - previous_percentage = percentage_to_append + def _product_data_to_vals(self, product_data, level, create=False): + self.ensure_one() + res = { + "computed_level_id": level.id + } + if create: + res.update({ + "product_id": product_data["product"].id, + "profile_id": self.id, + }) + return res - level_percentage = list(zip(levels, cum_percentages)) + @api.multi + def _compute_abc_classification(self): + to_compute = self.filtered((lambda p: p.profile_type == "sale_stock")) + remaining = self - to_compute + res = None + if remaining: + res = super( + AbcClassificationProfile, remaining + )._compute_abc_classification() + ProductClassification = self.env["abc.classification.product.level"] + for profile in to_compute: + product_list, total = profile._get_data() + existing_level_ids_to_remove = profile._get_existing_level_ids() + level_percentage = ( + profile._build_ordered_level_cumulative_percentage() + ) level, percentage = level_percentage.pop(0) previous_data = {} for i, product_data in enumerate(product_list): @@ -113,10 +254,13 @@ def _get_sort_key_percentage(rec): + previous_data["cumulative_percentage"] ) ) - if product_data["cumulative_percentage"] > 100: - raise UserError(_("Cumulative percentage greater than 100.")) + if float_round(product_data["cumulative_percentage"], 0) > 100: + raise UserError( + _("Cumulative percentage greater than 100.") + ) - # Compute ABC classification for the products based on the cumulative percentage + # Compute ABC classification for the products based on the + # cumulative percentage if ( product_data["cumulative_percentage"] > percentage @@ -126,19 +270,25 @@ def _get_sort_key_percentage(rec): product_abc_classification = product_data[ "product" - ].abc_product_classification_level_ids.filtered( - lambda p, profile: p.profile_id == profile.id + ].abc_classification_product_level_ids.filtered( + lambda p, prof=profile: p.profile_id == prof ) if product_abc_classification: - product_abc_classification.write({"computed_level_id": level.id}) + # The line is still significant... + existing_level_ids_to_remove.remove( + product_abc_classification.id + ) + if product_abc_classification.level_id != level: + vals = profile._product_data_to_vals( + product_data, level, create=False + ) + product_abc_classification.write(vals) else: - product_abc_classification = ProductClassification.create( - { - "product_id": product_data["product"].id, - "profile_id": profile.id, - "computed_level_id": level.id, - } + vals = profile._product_data_to_vals( + product_data, level, create=True ) - + ProductClassification.create(vals) previous_data = product_data + profile._purge_obsolete_level_values(existing_level_ids_to_remove) + return res diff --git a/product_abc_classification_sale_stock/models/product_template.py b/product_abc_classification_sale_stock/models/product_template.py deleted file mode 100644 index 137ab8832aa9..000000000000 --- a/product_abc_classification_sale_stock/models/product_template.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2021 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo import models - - -class ProductTemplate(models.Model): - - _inherit = "product.template" diff --git a/product_abc_classification_sale_stock/readme/CONTRIBUTORS.rst b/product_abc_classification_sale_stock/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000000..2223963b2124 --- /dev/null +++ b/product_abc_classification_sale_stock/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Lindsay Marion +* Laurent Mignon diff --git a/product_abc_classification_sale_stock/readme/DESCRIPTION.rst b/product_abc_classification_sale_stock/readme/DESCRIPTION.rst new file mode 100644 index 000000000000..d289b47a2423 --- /dev/null +++ b/product_abc_classification_sale_stock/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This modules includes an ABC analysis computation profile based +on the number of sale order lines delivered from a given date by product. diff --git a/product_abc_classification_sale_stock/readme/USAGE.rst b/product_abc_classification_sale_stock/readme/USAGE.rst new file mode 100644 index 000000000000..188a95ba3c76 --- /dev/null +++ b/product_abc_classification_sale_stock/readme/USAGE.rst @@ -0,0 +1,8 @@ +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 variants, and assign them a profile. +Then the cron classification will proceed to assign to these products one of the profile's levels. diff --git a/product_abc_classification_sale_stock/static/description/index.html b/product_abc_classification_sale_stock/static/description/index.html new file mode 100644 index 000000000000..2821f7cb9798 --- /dev/null +++ b/product_abc_classification_sale_stock/static/description/index.html @@ -0,0 +1,431 @@ + + + + + + +Product Abc Classification based on delivered products + + + +
+

Product Abc Classification based on delivered products

+ + +

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

+

This modules includes an ABC analysis computation profile based +on the number of sale order lines delivered from a given date 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 variants, and assign them a profile. +Then the cron classification will proceed to assign to these products one of the profile’s levels.

+
+
+

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
  • +
+
+
+

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_sale_stock/tests/test_abc_classification_profile.py b/product_abc_classification_sale_stock/tests/test_abc_classification_profile.py index 92ed630bae63..cb220d1ec020 100644 --- a/product_abc_classification_sale_stock/tests/test_abc_classification_profile.py +++ b/product_abc_classification_sale_stock/tests/test_abc_classification_profile.py @@ -7,47 +7,37 @@ from odoo.tests.common import SavepointCase -class TestAbcClassificationProfile(SavepointCase): +class TestABCClassificationProfile(SavepointCase): @classmethod def setUpClass(cls): - super(TestAbcClassificationProfile, cls).setUpClass() + super(TestABCClassificationProfile, cls).setUpClass() cls.partner = cls.env["res.partner"].create( {"name": "Unittest partner", "ref": "12344566777878"} ) - cls.warehouse_1 = cls.env.ref("stock.warehouse0") - cls.warehouse_1.write( + cls.warehouse = cls.env.ref("stock.warehouse0") + cls.warehouse.write( { "name": "Test Warehouse", "reception_steps": "one_step", - "delivery_steps": "pick_ship", + "delivery_steps": "ship_only", "code": "TST", } ) - cls.warehouse_1.pick_type_id.subcode = "PICK" - cls.warehouse_1.pick_type_id.groupbypartner = False - cls.warehouse_1.out_type_id.groupbypartner = True - cls.level_A = cls.env["abc.classification.level"].create( - {"name": "A", "percentage": 80} + cls.stock_profile = cls.env.ref( + "product_abc_classification_sale_stock." + "abc_classification_profile_sale_stock" ) - - cls.level_B = cls.env["abc.classification.level"].create( - {"name": "B", "percentage": 15} + cls.level_A = cls.env.ref( + "product_abc_classification_sale_stock.abc_classification_level_a" ) - - cls.level_C = cls.env["abc.classification.level"].create( - {"name": "C", "percentage": 5} + cls.level_B = cls.env.ref( + "product_abc_classification_sale_stock.abc_classification_level_b" ) - - cls.stock_profile = cls.env["abc.classification.profile"].create( - { - "name": "Stock profile", - "profile_type": "stock", - "period": 365, - "level_ids": [(6, 0, [cls.level_A.id, cls.level_B.id, cls.level_C.id])], - } + cls.level_C = cls.env.ref( + "product_abc_classification_sale_stock.abc_classification_level_c" ) cls.product1 = cls.env["product.product"].create( @@ -115,6 +105,17 @@ def setUpClass(cls): "abc_classification_profile_ids": [(4, cls.stock_profile.id)], } ) + # Special case where the product is not yet sold nor delivered + cls.product_new = cls.env["product.product"].create( + { + "name": "product_new", + "uom_id": cls.env.ref("product.product_uom_unit").id, + "type": "product", + "default_code": "345789733", + "tracking": "none", + "abc_classification_profile_ids": [(4, cls.stock_profile.id)], + } + ) cls._create_availability(cls.product1) cls._create_availability(cls.product2) @@ -125,41 +126,49 @@ def setUpClass(cls): cls.so1 = cls._confirm_sale_order( products=[cls.product1, cls.product2, cls.product3], - qty={cls.product1.name: 80, cls.product2.name: 10, cls.product3.name: 30}, + qty={ + cls.product1.name: 80, + cls.product2.name: 10, + cls.product3.name: 30, + }, ) - cls._confirm_pick_ship(cls.so1) + cls._confirm_ship(cls.so1) cls.so2 = cls._confirm_sale_order( products=[cls.product4, cls.product5, cls.product6], - qty={cls.product4.name: 5, cls.product5.name: 30, cls.product6.name: 25}, + qty={ + cls.product4.name: 5, + cls.product5.name: 30, + cls.product6.name: 25, + }, ) - cls._confirm_pick_ship(cls.so2) + cls._confirm_ship(cls.so2) cls.so3 = cls._confirm_sale_order( products=[cls.product1], qty={cls.product1.name: 75} ) - cls._confirm_pick_ship(cls.so3) + cls._confirm_ship(cls.so3) cls.so3 = cls._confirm_sale_order( products=[cls.product1], qty={cls.product1.name: 75} ) - cls._confirm_pick_ship(cls.so3) + cls._confirm_ship(cls.so3) cls.so4 = cls._confirm_sale_order( products=[cls.product1], qty={cls.product1.name: 25} ) - cls._confirm_pick_ship(cls.so4) + cls._confirm_ship(cls.so4) cls.so5 = cls._confirm_sale_order( products=[cls.product3, cls.product5], qty={cls.product3.name: 90, cls.product5.name: 50}, ) - cls._confirm_pick_ship(cls.so5) + cls._confirm_ship(cls.so5) cls.so6 = cls._confirm_sale_order( products=[cls.product6], qty={cls.product6.name: 30} ) - cls._confirm_pick_ship(cls.so6) + cls._confirm_ship(cls.so6) @classmethod def _create_availability(cls, product): @@ -168,7 +177,7 @@ def _create_availability(cls, product): "product_id": product.id, "product_tmpl_id": product.product_tmpl_id.id, "new_quantity": 500, - "location_id": cls.warehouse_1.lot_stock_id.id, + "location_id": cls.warehouse.lot_stock_id.id, } ) update_qty_wizard.change_product_qty() @@ -177,7 +186,7 @@ def _create_availability(cls, product): def _confirm_sale_order(cls, products, qty, partner=None): if partner is None: partner = cls.partner - warehouse = cls.warehouse_1 + warehouse = cls.warehouse Sale = cls.env["sale.order"] lines = [ ( @@ -203,71 +212,54 @@ def _confirm_sale_order(cls, products, qty, partner=None): return so @classmethod - def _confirm_pick_ship(cls, so): - pick = so.mapped("picking_ids").filtered( - lambda p: p.picking_type_subcode == "PICK" - ) + def _confirm_ship(cls, so): + pick = so.mapped("picking_ids") pick.action_confirm() pick.action_assign() for pack_op in pick.pack_operation_ids: pack_op.qty_done = pack_op.product_qty pick.action_done() - ship = so.mapped("picking_ids").filtered( - lambda p: p.picking_type_code == "outgoing" + + def _assertLevelIs(self, product, level_name): + levels = product.abc_classification_product_level_ids + self.assertEqual( + levels.computed_level_id.name, + level_name, + "{} should be classified as {}".format(product.name, level_name), + ) + levels = product.product_tmpl_id.abc_classification_product_level_ids + self.assertEqual( + levels.computed_level_id.name, + level_name, + "{} template should be classified as {}".format(product.name, level_name), ) - ship.action_confirm() - ship.action_assign() - for pack_op in ship.pack_operation_ids: - pack_op.qty_done = pack_op.product_qty - ship.action_done() @freeze_time("2021-01-01 07:10:00") def test_00(self): + # test computed classification and check that the classification is + # also set on the product_templale self.stock_profile._compute_abc_classification() - product_classification1 = self.env["abc.classification.product.level"].search( - [ - ("profile_id", "=", self.stock_profile.id), - ("product_id", "=", self.product1.id), - ] - ) - product_classification2 = self.env["abc.classification.product.level"].search( - [ - ("profile_id", "=", self.stock_profile.id), - ("product_id", "=", self.product2.id), - ] - ) - - product_classification3 = self.env["abc.classification.product.level"].search( - [ - ("profile_id", "=", self.stock_profile.id), - ("product_id", "=", self.product3.id), - ] - ) - - product_classification4 = self.env["abc.classification.product.level"].search( - [ - ("profile_id", "=", self.stock_profile.id), - ("product_id", "=", self.product4.id), - ] - ) + self._assertLevelIs(self.product1, "a") + self._assertLevelIs(self.product3, "a") + self._assertLevelIs(self.product5, "a") + self._assertLevelIs(self.product2, "b") + self._assertLevelIs(self.product6, "b") + self._assertLevelIs(self.product4, "c") + self._assertLevelIs(self.product_new, "c") - product_classification5 = self.env["abc.classification.product.level"].search( - [ - ("profile_id", "=", self.stock_profile.id), - ("product_id", "=", self.product5.id), - ] - ) - - product_classification6 = self.env["abc.classification.product.level"].search( - [ - ("profile_id", "=", self.stock_profile.id), - ("product_id", "=", self.product6.id), - ] - ) - - self.assertEqual(product_classification1.computed_level_id.name, "A") - self.assertEqual(product_classification3.computed_level_id.name, "A") - self.assertEqual(product_classification5.computed_level_id.name, "A") - self.assertEqual(product_classification2.computed_level_id.name, "B") - self.assertEqual(product_classification6.computed_level_id.name, "B") - self.assertEqual(product_classification4.computed_level_id.name, "C") + @freeze_time("2021-01-01 07:10:00") + def test_01(self): + # test computed classification and check that inactive products are + # not taken into account + self.product1.active = False + self.product1.refresh() + self.stock_profile._compute_abc_classification() + self.assertFalse(self.product1.abc_classification_product_level_ids) + self.product1.active = True + self.product1.refresh() + self.stock_profile._compute_abc_classification() + self.assertTrue(self.product1.abc_classification_product_level_ids) + self.product1.active = False + self.product1.refresh() + self.stock_profile._compute_abc_classification() + self.assertFalse(self.product1.abc_classification_product_level_ids) diff --git a/product_abc_classification_sale_stock/views/abc_classification_profile.xml b/product_abc_classification_sale_stock/views/abc_classification_profile.xml new file mode 100644 index 000000000000..ccaa0f9f654d --- /dev/null +++ b/product_abc_classification_sale_stock/views/abc_classification_profile.xml @@ -0,0 +1,15 @@ + + + + + abc.classification.profile.form (in product_abc_classification_sale_stock) + abc.classification.profile + + + + + + + + diff --git a/requirements.txt b/requirements.txt index 180fc49789ba..dbf0088e1146 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ # generated from manifests external_dependencies openupgradelib +freezegun==0.3.14 + diff --git a/setup/product_abc_classification_base/.eggs/README.txt b/setup/product_abc_classification_base/.eggs/README.txt new file mode 100644 index 000000000000..5d01668824f4 --- /dev/null +++ b/setup/product_abc_classification_base/.eggs/README.txt @@ -0,0 +1,6 @@ +This directory contains eggs that were downloaded by setuptools to build, test, and run plug-ins. + +This directory caches those eggs to prevent repeated downloads. + +However, it is safe to delete this directory. + diff --git a/setup/product_abc_classification_base/odoo/__init__.py b/setup/product_abc_classification_base/odoo/__init__.py new file mode 100644 index 000000000000..de40ea7ca058 --- /dev/null +++ b/setup/product_abc_classification_base/odoo/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/setup/product_abc_classification_base/odoo/addons/__init__.py b/setup/product_abc_classification_base/odoo/addons/__init__.py new file mode 100644 index 000000000000..de40ea7ca058 --- /dev/null +++ b/setup/product_abc_classification_base/odoo/addons/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/setup/product_abc_classification_base/odoo/addons/product_abc_classification_base b/setup/product_abc_classification_base/odoo/addons/product_abc_classification_base new file mode 120000 index 000000000000..ddbb5a97873a --- /dev/null +++ b/setup/product_abc_classification_base/odoo/addons/product_abc_classification_base @@ -0,0 +1 @@ +../../../../product_abc_classification_base \ No newline at end of file diff --git a/setup/product_abc_classification_base/setup.py b/setup/product_abc_classification_base/setup.py new file mode 100644 index 000000000000..28c57bb64031 --- /dev/null +++ b/setup/product_abc_classification_base/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/product_abc_classification_sale_stock/.eggs/README.txt b/setup/product_abc_classification_sale_stock/.eggs/README.txt new file mode 100644 index 000000000000..5d01668824f4 --- /dev/null +++ b/setup/product_abc_classification_sale_stock/.eggs/README.txt @@ -0,0 +1,6 @@ +This directory contains eggs that were downloaded by setuptools to build, test, and run plug-ins. + +This directory caches those eggs to prevent repeated downloads. + +However, it is safe to delete this directory. + diff --git a/setup/product_abc_classification_sale_stock/odoo/__init__.py b/setup/product_abc_classification_sale_stock/odoo/__init__.py new file mode 100644 index 000000000000..de40ea7ca058 --- /dev/null +++ b/setup/product_abc_classification_sale_stock/odoo/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/setup/product_abc_classification_sale_stock/odoo/addons/__init__.py b/setup/product_abc_classification_sale_stock/odoo/addons/__init__.py new file mode 100644 index 000000000000..de40ea7ca058 --- /dev/null +++ b/setup/product_abc_classification_sale_stock/odoo/addons/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/setup/product_abc_classification_sale_stock/odoo/addons/product_abc_classification_sale_stock b/setup/product_abc_classification_sale_stock/odoo/addons/product_abc_classification_sale_stock new file mode 120000 index 000000000000..b9bb5396d797 --- /dev/null +++ b/setup/product_abc_classification_sale_stock/odoo/addons/product_abc_classification_sale_stock @@ -0,0 +1 @@ +../../../../product_abc_classification_sale_stock \ No newline at end of file diff --git a/setup/product_abc_classification_sale_stock/setup.py b/setup/product_abc_classification_sale_stock/setup.py new file mode 100644 index 000000000000..28c57bb64031 --- /dev/null +++ b/setup/product_abc_classification_sale_stock/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From a90db80ab1806e407f5b23231249206b93ef0ea4 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Thu, 4 Feb 2021 11:50:56 +0100 Subject: [PATCH 04/21] [IMP] product_abc_classification_sale_stock: Store collected data used to compute the classification --- .../abc_classification_product_level.py | 6 +- .../abc_classification_product_level.xml | 3 +- .../__manifest__.py | 7 +- .../i18n/fr.po | 153 ++++++++++++++- .../models/__init__.py | 2 + .../abc_classification_product_level.py | 15 ++ .../models/abc_classification_profile.py | 177 +++++++++++++----- .../models/abc_sale_stock_level_history.py | 91 +++++++++ .../security/abc_sale_stock_level_history.xml | 18 ++ .../tests/test_abc_classification_profile.py | 12 ++ .../abc_classification_product_level.xml | 30 +++ .../views/abc_sale_stock_level_history.xml | 58 ++++++ 12 files changed, 525 insertions(+), 47 deletions(-) create mode 100644 product_abc_classification_sale_stock/models/abc_classification_product_level.py create mode 100644 product_abc_classification_sale_stock/models/abc_sale_stock_level_history.py create mode 100644 product_abc_classification_sale_stock/security/abc_sale_stock_level_history.xml create mode 100644 product_abc_classification_sale_stock/views/abc_classification_product_level.xml create mode 100644 product_abc_classification_sale_stock/views/abc_sale_stock_level_history.xml diff --git a/product_abc_classification_base/models/abc_classification_product_level.py b/product_abc_classification_base/models/abc_classification_product_level.py index f795051bf884..f8d111e10930 100644 --- a/product_abc_classification_base/models/abc_classification_product_level.py +++ b/product_abc_classification_base/models/abc_classification_product_level.py @@ -23,7 +23,6 @@ class AbcClassificationProductLevel(models.Model): computed_level_id = fields.Many2one( "abc.classification.level", string="Computed classification level", - track_visibility="onchange", readonly=True, ) level_id = fields.Many2one( @@ -60,6 +59,11 @@ class AbcClassificationProductLevel(models.Model): 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" diff --git a/product_abc_classification_base/views/abc_classification_product_level.xml b/product_abc_classification_base/views/abc_classification_product_level.xml index e4c7e0486fc0..b6ffa789ea3f 100644 --- a/product_abc_classification_base/views/abc_classification_product_level.xml +++ b/product_abc_classification_base/views/abc_classification_product_level.xml @@ -13,7 +13,7 @@ role="alert" attrs="{'invisible': [('flag','=',False)]}" >Computed level differs from the specified level - + @@ -23,6 +23,7 @@ +
diff --git a/product_abc_classification_sale_stock/__manifest__.py b/product_abc_classification_sale_stock/__manifest__.py index c60f80bcd0a0..dff75041bed5 100644 --- a/product_abc_classification_sale_stock/__manifest__.py +++ b/product_abc_classification_sale_stock/__manifest__.py @@ -11,7 +11,12 @@ "license": "AGPL-3", "author": "ACSONE SA/NV,Odoo Community Association (OCA)", "depends": ["product_abc_classification_base", "sale_stock"], - "data": ["views/abc_classification_profile.xml"], + "data": [ + "views/abc_classification_product_level.xml", + "security/abc_sale_stock_level_history.xml", + "views/abc_sale_stock_level_history.xml", + "views/abc_classification_profile.xml", + ], "demo": [ "demo/abc_classification_level.xml", "demo/abc_classification_profile.xml", diff --git a/product_abc_classification_sale_stock/i18n/fr.po b/product_abc_classification_sale_stock/i18n/fr.po index f9c51f29cdb8..74664c95320f 100644 --- a/product_abc_classification_sale_stock/i18n/fr.po +++ b/product_abc_classification_sale_stock/i18n/fr.po @@ -20,24 +20,173 @@ msgstr "" 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_sale_stock +#: model:ir.model,name:product_abc_classification_sale_stock.model_abc_classification_product_level +msgid "Abc Classification Product Level" +msgstr "Niveau de classification ABC d'un article'" + #. module: product_abc_classification_sale_stock #: model:ir.model,name:product_abc_classification_sale_stock.model_abc_classification_profile msgid "Abc Classification Profile" msgstr "Profil de classification ABC" #. module: product_abc_classification_sale_stock -#: code:addons/product_abc_classification_sale_stock/models/abc_classification_profile.py:247 +#: model:ir.actions.act_window,name:product_abc_classification_sale_stock.abc_sale_stock_level_history_act_window +#: model:ir.model,name:product_abc_classification_sale_stock.model_abc_sale_stock_level_history +#: model:ir.ui.menu,name:product_abc_classification_sale_stock.abc_sale_stock_level_history_menu +msgid "Abc Sale_stock Level History" +msgstr "ABC: Historique des données de calul (basé sur les ventes)" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_computed_level_id +msgid "Computed classification level" +msgstr "Classe calculée" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_create_uid +msgid "Created by" +msgstr "Créé par" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_create_date +msgid "Created on" +msgstr "Créé le" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_cumulated_percentage +msgid "Cumulated percentage" +msgstr "Poucentage cumulé" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,help:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_cumulated_percentage +msgid "Cumulated percentage of all the products with a better ranking" +msgstr "Pourcentage cumulé de tous les articles avec un meilleur classement" + +#. module: product_abc_classification_sale_stock +#: code:addons/product_abc_classification_sale_stock/models/abc_classification_profile.py:273 #, python-format msgid "Cumulative percentage greater than 100." msgstr "Somme des pourcentages calculés supérieure à 100." +#. module: product_abc_classification_sale_stock +#: model:ir.ui.view,arch_db:product_abc_classification_sale_stock.abc_sale_stock_level_history_search_view +msgid "Data collected this year" +msgstr "Données collectées cette année" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_from_date +msgid "From date" +msgstr "De" + +#. module: product_abc_classification_sale_stock +#: model:ir.ui.view,arch_db:product_abc_classification_sale_stock.abc_sale_stock_level_history_search_view +msgid "Group By" +msgstr "Grouper par" + +#. module: product_abc_classification_sale_stock +#: model:ir.ui.view,arch_db:product_abc_classification_sale_stock.abc_classification_product_level_form_view +msgid "History" +msgstr "Historique" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_id +msgid "ID" +msgstr "ID" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history___last_update +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_write_uid +msgid "Last Updated by" +msgstr "Dernière mise à jour par" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_write_date +msgid "Last Updated on" +msgstr "Dernière mise à jour le" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_number_of_so_lines +msgid "Number of sale order lines" +msgstr "Nombre de ligne de vente" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_percentage +msgid "Percentage" +msgstr "Pourcentage" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,help:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_percentage +msgid "Percentage of total sale order lines" +msgstr "Pourcentage du nombre total de toutes les lignes de ventes" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_product_id +msgid "Product" +msgstr "Article" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_product_level_id +msgid "Product level id" +msgstr "Classement ABC de l'article" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_product_tmpl_id +msgid "Product template" +msgstr "Modèle de produit" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_profile_id +#: model:ir.ui.view,arch_db:product_abc_classification_sale_stock.abc_sale_stock_level_history_search_view +msgid "Profile" +msgstr "Profil" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_ranking +msgid "Ranking" +msgstr "Classement" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,help:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_ranking +msgid "Ranking by number of oder lines" +msgstr "Classement par nombre de lignes de vente" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_classification_product_level_sale_stock_level_history_ids +msgid "Sale stock level history ids" +msgstr "Historique des données de vente collectées" + +#. module: product_abc_classification_sale_stock +#: model:ir.ui.view,arch_db:product_abc_classification_sale_stock.abc_sale_stock_level_history_search_view +msgid "This Year" +msgstr "Cette année" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_to_date +msgid "To date" +msgstr "A" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_total_of_so_lines +msgid "Total of sale order lines" +msgstr "Total des lignes de vente" + #. module: product_abc_classification_sale_stock #: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_classification_profile_warehouse_id +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_warehouse_id msgid "Warehouse" msgstr "Entrepôt" #. module: product_abc_classification_sale_stock -#: code:addons/product_abc_classification_sale_stock/models/abc_classification_profile.py:39 +#: code:addons/product_abc_classification_sale_stock/models/abc_classification_profile.py:41 #, python-format msgid "You must specify a warehouse for {profile_name}" msgstr "Vous devez specifier un entrepôt pour le profil {profile_name}" diff --git a/product_abc_classification_sale_stock/models/__init__.py b/product_abc_classification_sale_stock/models/__init__.py index 3737cdc08877..ba6399b8dd63 100644 --- a/product_abc_classification_sale_stock/models/__init__.py +++ b/product_abc_classification_sale_stock/models/__init__.py @@ -1 +1,3 @@ from . import abc_classification_profile +from . import abc_sale_stock_level_history +from . import abc_classification_product_level diff --git a/product_abc_classification_sale_stock/models/abc_classification_product_level.py b/product_abc_classification_sale_stock/models/abc_classification_product_level.py new file mode 100644 index 000000000000..c59b9740ead3 --- /dev/null +++ b/product_abc_classification_sale_stock/models/abc_classification_product_level.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class AbcClassificationProductLevel(models.Model): + + _inherit = "abc.classification.product.level" + + sale_stock_level_history_ids = fields.One2many( + comodel_name="abc.sale_stock.level.history", + inverse_name="product_level_id", + ) diff --git a/product_abc_classification_sale_stock/models/abc_classification_profile.py b/product_abc_classification_sale_stock/models/abc_classification_profile.py index 47cce0687165..239058fcbcf2 100644 --- a/product_abc_classification_sale_stock/models/abc_classification_profile.py +++ b/product_abc_classification_sale_stock/models/abc_classification_profile.py @@ -2,6 +2,8 @@ # Copyright 2021 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import csv +from cStringIO import StringIO from datetime import datetime, timedelta from operator import attrgetter @@ -41,11 +43,15 @@ def _check_warehouse_id(self): ).forman(profile_name=rec.name) ) - def _fill_initial_product_data(self, date): - product_list = [] - if self.profile_type == "sale_stock": - return self._fill_data(date, product_list) - return product_list, 0 + @api.model + def _get_collected_data_class(self): + return SaleStockData + + def _init_collected_data_instance(self): + self.ensure_one() + sale_stock_data = self._get_collected_data_class()() + sale_stock_data.profile = self + return sale_stock_data def _get_all_product_ids(self): """Get a set of product ids with the current profile""" @@ -78,6 +84,7 @@ def _get_data(self, from_date=None): datetime.today() - timedelta(days=self.period) ) ) + to_date = datetime.today() customer_location_ids = ( self.env["stock.location"].search([("usage", "=", "customer")]).ids ) @@ -135,25 +142,33 @@ def _get_data(self, from_date=None): result = self.env.cr.fetchall() total = 0 - product_list = [] + sale_stock_data_list = [] + ranking = 1 + ProductProduct = self.env["product.product"] for r in result: + sale_stock_data = self._init_collected_data_instance() product_id = r[0] - product_data = { - "product": self.env["product.product"].browse(product_id), - "number_of_so_lines": int(r[1]), - } + sale_stock_data.product = ProductProduct.browse(product_id) + sale_stock_data.number_of_so_lines = int(r[1]) + sale_stock_data.ranking = ranking + sale_stock_data.from_date = from_date + sale_stock_data.to_date = to_date + ranking += 1 total += int(r[1]) - product_list.append(product_data) + sale_stock_data_list.append(sale_stock_data) all_product_ids.remove(product_id) + # Add all products not sold or not delivered into this timelapse for product_id in all_product_ids: - product_list.append( - { - "product": self.env["product.product"].browse(product_id), - "number_of_so_lines": 0, - } - ) - return product_list, total + sale_stock_data = self._init_collected_data_instance() + sale_stock_data.product = ProductProduct.browse(product_id) + sale_stock_data.number_of_so_lines = 0 + sale_stock_data.ranking = ranking + sale_stock_data.from_date = from_date + sale_stock_data.to_date = to_date + sale_stock_data_list.append(sale_stock_data) + + return sale_stock_data_list, total def _build_ordered_level_cumulative_percentage(self): """Return an ordered list of tuple of level, cumulative percentage @@ -206,15 +221,15 @@ def _purge_obsolete_level_values(self, ids_to_remove): {"ids": tuple(ids_to_remove)}, ) - def _product_data_to_vals(self, product_data, level, create=False): + def _sale_stock_data_to_vals(self, sale_stock_data, create=False): self.ensure_one() res = { - "computed_level_id": level.id + "computed_level_id": sale_stock_data.computed_level.id } if create: res.update({ - "product_id": product_data["product"].id, - "profile_id": self.id, + "product_id": sale_stock_data.product.id, + "profile_id": sale_stock_data.profile.id, }) return res @@ -230,31 +245,30 @@ def _compute_abc_classification(self): ProductClassification = self.env["abc.classification.product.level"] for profile in to_compute: - product_list, total = profile._get_data() + sale_stock_data_list, total = profile._get_data() existing_level_ids_to_remove = profile._get_existing_level_ids() level_percentage = ( profile._build_ordered_level_cumulative_percentage() ) level, percentage = level_percentage.pop(0) previous_data = {} - for i, product_data in enumerate(product_list): - + for i, sale_stock_data in enumerate(sale_stock_data_list): # Compute percentages and cumulative percentages for the products - product_data["number_of_so_lines_percentage"] = ( - (100.0 * product_data["number_of_so_lines"] / total) + sale_stock_data.percentage = ( + (100.0 * sale_stock_data.number_of_so_lines / total) if total else 0.0 ) - product_data["cumulative_percentage"] = ( - product_data["number_of_so_lines_percentage"] + sale_stock_data.cumulated_percentage = ( + sale_stock_data.percentage if i == 0 else ( - product_data["number_of_so_lines_percentage"] - + previous_data["cumulative_percentage"] + sale_stock_data.percentage + + previous_data.cumulated_percentage ) ) - if float_round(product_data["cumulative_percentage"], 0) > 100: + if float_round(sale_stock_data.cumulated_percentage, 0) > 100: raise UserError( _("Cumulative percentage greater than 100.") ) @@ -263,32 +277,111 @@ def _compute_abc_classification(self): # cumulative percentage if ( - product_data["cumulative_percentage"] > percentage + sale_stock_data.cumulated_percentage > percentage and len(level_percentage) > 0 ): level, percentage = level_percentage.pop(0) - product_abc_classification = product_data[ - "product" - ].abc_classification_product_level_ids.filtered( + product = sale_stock_data.product + levels = product.abc_classification_product_level_ids + product_abc_classification = levels.filtered( lambda p, prof=profile: p.profile_id == prof ) + sale_stock_data.computed_level = level if product_abc_classification: # The line is still significant... existing_level_ids_to_remove.remove( product_abc_classification.id ) if product_abc_classification.level_id != level: - vals = profile._product_data_to_vals( - product_data, level, create=False + vals = profile._sale_stock_data_to_vals( + sale_stock_data, create=False ) product_abc_classification.write(vals) else: - vals = profile._product_data_to_vals( - product_data, level, create=True + vals = profile._sale_stock_data_to_vals( + sale_stock_data, create=True + ) + product_abc_classification = ProductClassification.create( + vals ) - ProductClassification.create(vals) - previous_data = product_data + sale_stock_data.total_of_so_lines = total + sale_stock_data.product_level = product_abc_classification + previous_data = sale_stock_data + self._log_history(sale_stock_data_list) profile._purge_obsolete_level_values(existing_level_ids_to_remove) return res + + def _log_history(self, sale_stock_data_list): + """ Log collected and computed values into + abc.sale_stock.level.history + + """ + vals = StringIO() + writer = csv.writer(vals, delimiter=";") + for sale_stock_data in sale_stock_data_list: + writer.writerow(sale_stock_data._to_csv_line()) + vals.seek(0) + table = self.env["abc.sale_stock.level.history"]._table + columns = sale_stock_data._get_col_names() + self.env.cr.copy_from(vals, table, columns=columns, sep=";") + self.env["abc.classification.product.level"].invalidate_cache( + ["sale_stock_level_history_ids"] + ) + + +class SaleStockData(object): + """ Sale stock collected data + + This class is used to store all the data collectd and computed for + a abc classification product level. It also provide methods used to bulk + insert these data into the abc.sale_stock.level.history table. + + """ + __slots__ = [ + "product", "profile", "computed_level", "ranking", "percentage", + "cumulated_percentage", "number_of_so_lines", "total_of_so_lines", + "product_level", "from_date", "to_date" + ] + + def _to_csv_line(self): + """Return values to write into a csv file""" + return [ + self.product.id, + self.product.product_tmpl_id.id, + self.profile.id, + self.computed_level.id, + self.profile.warehouse_id.id, + self.ranking, + self.percentage, + self.cumulated_percentage, + self.number_of_so_lines, + self.total_of_so_lines, + self.product_level.id, + self.from_date, + self.to_date + ] + + @classmethod + def _get_col_names(cls): + """Return the ordered list of column names related to the values + returned by _to_csv_line + + We use the name of the columns defined into abc.sale_stock.level.history + """ + return [ + "product_id", + "product_tmpl_id", + "profile_id", + "computed_level_id", + "warehouse_id", + "ranking", + "percentage", + "cumulated_percentage", + "number_of_so_lines", + "total_of_so_lines", + "product_level_id", + "from_date", + "to_date" + ] diff --git a/product_abc_classification_sale_stock/models/abc_sale_stock_level_history.py b/product_abc_classification_sale_stock/models/abc_sale_stock_level_history.py new file mode 100644 index 000000000000..b0b66270ba08 --- /dev/null +++ b/product_abc_classification_sale_stock/models/abc_sale_stock_level_history.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class AbcSaleStockLevelHistory(models.Model): + """ ABC Classification Product Level History + + This model is used to display the history of values collected and involved + into the computation of the ABC classification level. + + To avoid performance issue, the table is populated by bypassing the ORM + since a new line is inserted by product and classification profile, + each time the computation of the classification levels occurs. + + Some could argue that the same functionality could be achieved by using the + tracking of changes mechanism provided by mail.thread. Nevertheless, + mail.thread introduce a to high performance footprint and the result is not + usable into reports + """ + + _name = "abc.sale_stock.level.history" + _description = "Abc Sale_stock Level History" + + computed_level_id = fields.Many2one( + "abc.classification.level", + string="Computed classification level", + readonly=True, + ondelete="cascade", + ) + product_id = fields.Many2one( + "product.product", + string="Product", + index=True, + required=True, + readonly=True, + ondelete="cascade", + ) + product_tmpl_id = fields.Many2one( + "product.template", + string="Product template", + related="product_id.product_tmpl_id", + readonly=True, + store=True, + ) + # percentage + profile_id = fields.Many2one( + "abc.classification.profile", + string="Profile", + required=True, + readonly=True, + ondelete="cascade", + ) + product_level_id = fields.Many2one( + "abc.classification.product.level", + required=True, + index=True, + readonly=True, + ondelete="cascade", + ) + warehouse_id = fields.Many2one( + "stock.warehouse", "Warehouse", readonly=False, ondelete="cascade", + ) + ranking = fields.Integer( + "Ranking", + required=True, + readonly=True, + help="Ranking by number of oder lines", + ) + number_of_so_lines = fields.Integer( + "Number of sale order lines", required=True, readonly=True, + ) + total_of_so_lines = fields.Integer( + "Total of sale order lines", required=True, readonly=True, + ) + percentage = fields.Float( + "Percentage", + required=True, + readonly=True, + help="Percentage of total sale order lines", + ) + cumulated_percentage = fields.Float( + "Cumulated percentage", + required=True, + readonly=True, + help="Cumulated percentage of all the products with a better ranking", + ) + from_date = fields.Date(readonly=True) + to_date = fields.Date(readonly=True) diff --git a/product_abc_classification_sale_stock/security/abc_sale_stock_level_history.xml b/product_abc_classification_sale_stock/security/abc_sale_stock_level_history.xml new file mode 100644 index 000000000000..0b2310cf480f --- /dev/null +++ b/product_abc_classification_sale_stock/security/abc_sale_stock_level_history.xml @@ -0,0 +1,18 @@ + + + + + + + abc.sale_stock.level.history access name + + + + + + + + + + diff --git a/product_abc_classification_sale_stock/tests/test_abc_classification_profile.py b/product_abc_classification_sale_stock/tests/test_abc_classification_profile.py index cb220d1ec020..faa97490184d 100644 --- a/product_abc_classification_sale_stock/tests/test_abc_classification_profile.py +++ b/product_abc_classification_sale_stock/tests/test_abc_classification_profile.py @@ -263,3 +263,15 @@ def test_01(self): self.product1.refresh() self.stock_profile._compute_abc_classification() self.assertFalse(self.product1.abc_classification_product_level_ids) + + @freeze_time("2021-01-01 07:10:00") + def test_02(self): + # check that a line is created into the history value for each + # computed classification level each time a compute is done + levels = self.product1.abc_classification_product_level_ids + self.assertFalse(levels.sale_stock_level_history_ids) + self.stock_profile._compute_abc_classification() + levels = self.product1.abc_classification_product_level_ids + self.assertEqual(len(levels.sale_stock_level_history_ids), 1) + self.stock_profile._compute_abc_classification() + self.assertEqual(len(levels.sale_stock_level_history_ids), 2) diff --git a/product_abc_classification_sale_stock/views/abc_classification_product_level.xml b/product_abc_classification_sale_stock/views/abc_classification_product_level.xml new file mode 100644 index 000000000000..e1a2afab086c --- /dev/null +++ b/product_abc_classification_sale_stock/views/abc_classification_product_level.xml @@ -0,0 +1,30 @@ + + + + + + + abc.classification.product.level.form (in product_abc_classification_sale_stock) + abc.classification.product.level + + + + + + + + + + + + + + + + + + + + + diff --git a/product_abc_classification_sale_stock/views/abc_sale_stock_level_history.xml b/product_abc_classification_sale_stock/views/abc_sale_stock_level_history.xml new file mode 100644 index 000000000000..c4a7413c1493 --- /dev/null +++ b/product_abc_classification_sale_stock/views/abc_sale_stock_level_history.xml @@ -0,0 +1,58 @@ + + + + + + + abc.sale_stock.level.history.search (in product_abc_classification_sale_stock) + abc.sale_stock.level.history + + + + + + + + + + + + + + + abc.sale_stock.level.history.tree (in product_abc_classification_sale_stock) + abc.sale_stock.level.history + + + + + + + + + + + + + + + + + + Abc Sale_stock Level History + abc.sale_stock.level.history + tree,form + [] + {'search_default_thisyear': 1} + + + + Abc Sale_stock Level History + + + + + + + From cad78fe2579387eecf5e562c820e6314f2106882 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Mon, 15 Feb 2021 17:41:52 +0100 Subject: [PATCH 05/21] [IMP] product_abc_classification: Take into a account the expected distribution of produts --- product_abc_classification_base/i18n/fr.po | 71 ++++++-- .../models/abc_classification_level.py | 19 ++- .../models/abc_classification_profile.py | 11 ++ .../tests/common.py | 40 ++++- .../tests/test_abc_classification_profile.py | 160 ++++++++++++++++-- .../views/abc_classification_profile.xml | 1 + .../demo/abc_classification_level.xml | 3 + .../i18n/fr.po | 54 ++++-- .../models/abc_classification_profile.py | 90 +++++++--- .../models/abc_sale_stock_level_history.py | 38 ++++- .../tests/test_abc_classification_profile.py | 4 +- .../abc_classification_product_level.xml | 4 +- .../views/abc_sale_stock_level_history.xml | 8 +- 13 files changed, 420 insertions(+), 83 deletions(-) diff --git a/product_abc_classification_base/i18n/fr.po b/product_abc_classification_base/i18n/fr.po index 93eb0ee151a0..4baea279b742 100644 --- a/product_abc_classification_base/i18n/fr.po +++ b/product_abc_classification_base/i18n/fr.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 10.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-02-03 06:57+0000\n" -"PO-Revision-Date: 2021-02-03 06:57+0000\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" @@ -15,6 +15,16 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" +#. module: product_abc_classification_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level_percentage +msgid "% Indicator" +msgstr "% KPI + +#. module: product_abc_classification_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level_percentage_products +msgid "% Products" +msgstr "% Articles" + #. module: product_abc_classification_base #: model:ir.ui.view,arch_db:product_abc_classification_base.product_template_form_view msgid "ABC Classification" @@ -22,7 +32,6 @@ msgstr "Classification ABC" #. module: product_abc_classification_base #: model:ir.ui.view,arch_db:product_abc_classification_base.abc_classification_product_level_form_view -#: model:ir.model,name:product_abc_classification_base.model_abc_classification_product_level msgid "ABC Classification Product Level" msgstr "Niveau de classification ABC des articles" @@ -42,6 +51,10 @@ msgstr "Profil ABC" msgid "ABC Profiles" msgstr "Profils ABC" +#. module: product_abc_classification_base +#: model:ir.model,name:product_abc_classification_base.model_abc_classification_product_level +msgid "Abc Classification Product Level" +msgstr "Niveau de classification" #. module: product_abc_classification_base #: model:ir.model,name:product_abc_classification_base.model_abc_classification_profile @@ -68,6 +81,15 @@ msgstr "Classes ABC" msgid "Abc classification profile ids" msgstr "Profils ABC" +#. module: product_abc_classification_base +#: 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_base +#: model:ir.model.fields,help:product_abc_classification_base.field_abc_classification_level_name +msgid "Classification A, B or C" +msgstr "Classification A, B ou C" #. module: product_abc_classification_base #: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_level_id @@ -75,7 +97,7 @@ msgid "Classification level" msgstr "Classe / Niveau" #. module: product_abc_classification_base -#: code:addons/product_abc_classification_base/models/abc_classification_product_level.py:75 +#: code:addons/product_abc_classification_base/models/abc_classification_product_level.py:84 #, python-format msgid "Classification level is mandatory" msgstr "La classe / niveau est obligatoire" @@ -96,7 +118,7 @@ msgid "Computed level differs from the specified level" msgstr "La class calculée diverge de la valeur spécifiée" #. module: product_abc_classification_base -#: code:addons/product_abc_classification_base/models/abc_classification_product_level.py:81 +#: code:addons/product_abc_classification_base/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" @@ -163,7 +185,7 @@ msgstr "Dernière mise à jour le" #. module: product_abc_classification_base #: model:ir.ui.view,arch_db:product_abc_classification_base.abc_classification_product_level_search_view msgid "Level" -msgstr "Classe" +msgstr "Niveau" #. module: product_abc_classification_base #: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_level_ids @@ -172,7 +194,7 @@ msgstr "Classes" #. module: product_abc_classification_base #: sql_constraint:abc.classification.level:0 -#: code:addons/product_abc_classification_base/models/abc_classification_level.py:26 +#: code:addons/product_abc_classification_base/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" @@ -183,7 +205,7 @@ msgid "Manual classification level" msgstr "Classe (Valeur à utiliser)" #. module: product_abc_classification_base -#: code:addons/product_abc_classification_base/models/abc_classification_product_level.py:91 +#: code:addons/product_abc_classification_base/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" @@ -196,7 +218,7 @@ msgstr "Nom" #. module: product_abc_classification_base #: sql_constraint:abc.classification.product.level:0 -#: code:addons/product_abc_classification_base/models/abc_classification_product_level.py:67 +#: code:addons/product_abc_classification_base/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." @@ -241,36 +263,55 @@ msgstr "Profil" #. module: product_abc_classification_base #: sql_constraint:abc.classification.profile:0 -#: code:addons/product_abc_classification_base/models/abc_classification_profile.py:32 +#: code:addons/product_abc_classification_base/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_base -#: code:addons/product_abc_classification_base/models/abc_classification_level.py:35 +#: code:addons/product_abc_classification_base/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_base -#: code:addons/product_abc_classification_base/models/abc_classification_level.py:39 +#: code:addons/product_abc_classification_base/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_base +#: code:addons/product_abc_classification_base/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_base +#: code:addons/product_abc_classification_base/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_base -#: code:addons/product_abc_classification_base/models/abc_classification_profile.py:51 +#: code:addons/product_abc_classification_base/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_base -#: code:addons/product_abc_classification_base/models/abc_classification_profile.py:42 +#: code:addons/product_abc_classification_base/models/abc_classification_profile.py:43 #, python-format msgid "The sum of the percentages of the levels should be 100." -msgstr "La somme des pourcentage ne doit pas dépasser 100." +msgstr "La somme des pourcentages ne doit pas dépasser 100." + +#. module: product_abc_classification_base +#: code:addons/product_abc_classification_base/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_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_profile_type #: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_profile_type msgid "Type of ABC classification" msgstr "Type de classification ABC" diff --git a/product_abc_classification_base/models/abc_classification_level.py b/product_abc_classification_base/models/abc_classification_level.py index 85bd7781a6d8..49ff40086fab 100644 --- a/product_abc_classification_base/models/abc_classification_level.py +++ b/product_abc_classification_base/models/abc_classification_level.py @@ -13,7 +13,10 @@ class AbcClassificationLevel(models.Model): _order = "percentage desc, id desc" _rec_name = "name" - percentage = fields.Float(default=0.0, required=True, string="%") + 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" ) @@ -39,3 +42,17 @@ def _check_percentage(self): 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_base/models/abc_classification_profile.py b/product_abc_classification_base/models/abc_classification_profile.py index 552aaa7a0284..90b7bc614c22 100644 --- a/product_abc_classification_base/models/abc_classification_profile.py +++ b/product_abc_classification_base/models/abc_classification_profile.py @@ -51,6 +51,17 @@ def _check_levels(self): 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." + ) + ) @api.multi def _compute_abc_classification(self): diff --git a/product_abc_classification_base/tests/common.py b/product_abc_classification_base/tests/common.py index fc7645a34c87..823f564e69a9 100644 --- a/product_abc_classification_base/tests/common.py +++ b/product_abc_classification_base/tests/common.py @@ -26,8 +26,24 @@ def setUpClass(cls): cls.classification_profile.write( { "level_ids": [ - (0, 0, {"percentage": 60, "name": "a"}), - (0, 0, {"percentage": 40, "name": "b"}), + ( + 0, + 0, + { + "percentage": 60, + "percentage_products": 40, + "name": "a", + }, + ), + ( + 0, + 0, + { + "percentage": 40, + "percentage_products": 60, + "name": "b", + }, + ), ] } ) @@ -40,8 +56,24 @@ def setUpClass(cls): "name": "Profile test bis", "profile_type": "test_type", "level_ids": [ - (0, 0, {"percentage": 80, "name": "a"}), - (0, 0, {"percentage": 20, "name": "b"}), + ( + 0, + 0, + { + "percentage": 80, + "percentage_products": 40, + "name": "a", + }, + ), + ( + 0, + 0, + { + "percentage": 20, + "percentage_products": 60, + "name": "b", + }, + ), ], } ) diff --git a/product_abc_classification_base/tests/test_abc_classification_profile.py b/product_abc_classification_base/tests/test_abc_classification_profile.py index 6538946862a1..fd6c6a6bac71 100644 --- a/product_abc_classification_base/tests/test_abc_classification_profile.py +++ b/product_abc_classification_base/tests/test_abc_classification_profile.py @@ -21,8 +21,24 @@ def test_00(self): self.classification_profile.write( { "level_ids": [ - (0, 0, {"percentage": 60, "name": "A"}), - (0, 0, {"percentage": 40, "name": "B"}), + ( + 0, + 0, + { + "percentage": 60, + "percentage_products": 40, + "name": "A", + }, + ), + ( + 0, + 0, + { + "percentage": 40, + "percentage_products": 60, + "name": "B", + }, + ), ] } ) @@ -41,8 +57,24 @@ def test_01(self): self.classification_profile.write( { "level_ids": [ - (0, 0, {"percentage": 60, "name": "A"}), - (0, 0, {"percentage": 30, "name": "B"}), + ( + 0, + 0, + { + "percentage": 60, + "percentage_products": 40, + "name": "A", + }, + ), + ( + 0, + 0, + { + "percentage": 30, + "percentage_products": 60, + "name": "B", + }, + ), ] } ) @@ -60,8 +92,24 @@ def test_02(self): self.classification_profile.write( { "level_ids": [ - (0, 0, {"percentage": 60, "name": "A"}), - (0, 0, {"percentage": 50, "name": "B"}), + ( + 0, + 0, + { + "percentage": 60, + "percentage_products": 40, + "name": "A", + }, + ), + ( + 0, + 0, + { + "percentage": 50, + "percentage_products": 60, + "name": "B", + }, + ), ] } ) @@ -79,8 +127,24 @@ def test_03(self): self.classification_profile.write( { "level_ids": [ - (0, 0, {"percentage": 50, "name": "A"}), - (0, 0, {"percentage": 50, "name": "B"}), + ( + 0, + 0, + { + "percentage": 50, + "percentage_products": 40, + "name": "A", + }, + ), + ( + 0, + 0, + { + "percentage": 50, + "percentage_products": 60, + "name": "B", + }, + ), ] } ) @@ -99,8 +163,24 @@ def test_04(self): self.classification_profile.write( { "level_ids": [ - (0, 0, {"percentage": 150, "name": "A"}), - (0, 0, {"percentage": -50, "name": "B"}), + ( + 0, + 0, + { + "percentage": 150, + "percentage_products": 40, + "name": "A", + }, + ), + ( + 0, + 0, + { + "percentage": -50, + "percentage_products": 60, + "name": "B", + }, + ), ] } ) @@ -118,8 +198,24 @@ def test_05(self): self.classification_profile.write( { "level_ids": [ - (0, 0, {"percentage": 60, "name": "A"}), - (0, 0, {"percentage": 40, "name": "A"}), + ( + 0, + 0, + { + "percentage": 60, + "percentage_products": 40, + "name": "A", + }, + ), + ( + 0, + 0, + { + "percentage": 40, + "percentage_products": 60, + "name": "A", + }, + ), ] } ) @@ -137,8 +233,24 @@ def test_06(self): self.classification_profile.write( { "level_ids": [ - (0, 0, {"percentage": 60, "name": "A"}), - (0, 0, {"percentage": 40, "name": "B"}), + ( + 0, + 0, + { + "percentage": 60, + "percentage_products": 40, + "name": "A", + }, + ), + ( + 0, + 0, + { + "percentage": 40, + "percentage_products": 60, + "name": "B", + }, + ), ] } ) @@ -147,8 +259,24 @@ def test_06(self): "name": "New Profile test", "profile_type": "test_type", "level_ids": [ - (0, 0, {"percentage": 60, "name": "A"}), - (0, 0, {"percentage": 40, "name": "B"}), + ( + 0, + 0, + { + "percentage": 60, + "percentage_products": 40, + "name": "A", + }, + ), + ( + 0, + 0, + { + "percentage": 40, + "percentage_products": 60, + "name": "B", + }, + ), ], } ) diff --git a/product_abc_classification_base/views/abc_classification_profile.xml b/product_abc_classification_base/views/abc_classification_profile.xml index cff772fc1d79..1831db224cdb 100644 --- a/product_abc_classification_base/views/abc_classification_profile.xml +++ b/product_abc_classification_base/views/abc_classification_profile.xml @@ -12,6 +12,7 @@ + diff --git a/product_abc_classification_sale_stock/demo/abc_classification_level.xml b/product_abc_classification_sale_stock/demo/abc_classification_level.xml index d1c93d1902a6..6777e316af8e 100644 --- a/product_abc_classification_sale_stock/demo/abc_classification_level.xml +++ b/product_abc_classification_sale_stock/demo/abc_classification_level.xml @@ -5,13 +5,16 @@ a 80 + 20 b 15 + 30 c 5 + 50 diff --git a/product_abc_classification_sale_stock/i18n/fr.po b/product_abc_classification_sale_stock/i18n/fr.po index 74664c95320f..5c53a3d8b9fd 100644 --- a/product_abc_classification_sale_stock/i18n/fr.po +++ b/product_abc_classification_sale_stock/i18n/fr.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 10.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-02-03 07:21+0000\n" -"PO-Revision-Date: 2021-02-03 07:21+0000\n" +"POT-Creation-Date: 2021-02-15 16:51+0000\n" +"PO-Revision-Date: 2021-02-15 16:51+0000\n" "Last-Translator: <>\n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -15,15 +15,10 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" -#. module: product_abc_classification_sale_stock -#: 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_sale_stock #: model:ir.model,name:product_abc_classification_sale_stock.model_abc_classification_product_level msgid "Abc Classification Product Level" -msgstr "Niveau de classification ABC d'un article'" +msgstr "Class / Niveau ABC" #. module: product_abc_classification_sale_stock #: model:ir.model,name:product_abc_classification_sale_stock.model_abc_classification_profile @@ -55,7 +50,7 @@ msgstr "Créé le" #. module: product_abc_classification_sale_stock #: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_cumulated_percentage msgid "Cumulated percentage" -msgstr "Poucentage cumulé" +msgstr "Pourcentage cumulé" #. module: product_abc_classification_sale_stock #: model:ir.model.fields,help:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_cumulated_percentage @@ -63,7 +58,17 @@ msgid "Cumulated percentage of all the products with a better ranking" msgstr "Pourcentage cumulé de tous les articles avec un meilleur classement" #. module: product_abc_classification_sale_stock -#: code:addons/product_abc_classification_sale_stock/models/abc_classification_profile.py:273 +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_cumulated_percentage_products +msgid "Cumulated percentage of products" +msgstr "Pourcentage cumulé des articles" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,help:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_cumulated_percentage_products +msgid "Cumulated percentage of total products analyzed with a better ranking" +msgstr "Pourcentage de tous les article avec un meilleur classement" + +#. module: product_abc_classification_sale_stock +#: code:addons/product_abc_classification_sale_stock/models/abc_classification_profile.py:285 #, python-format msgid "Cumulative percentage greater than 100." msgstr "Somme des pourcentages calculés supérieure à 100." @@ -114,7 +119,7 @@ msgid "Last Updated on" msgstr "Dernière mise à jour le" #. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_number_of_so_lines +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_number_so_lines msgid "Number of sale order lines" msgstr "Nombre de ligne de vente" @@ -123,6 +128,16 @@ msgstr "Nombre de ligne de vente" msgid "Percentage" msgstr "Pourcentage" +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_percentage_products +msgid "Percentage of products" +msgstr "Pourcentage des articles" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,help:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_percentage_products +msgid "Percentage of total products analyzed" +msgstr "Percentage of total products analyzed" + #. module: product_abc_classification_sale_stock #: model:ir.model.fields,help:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_percentage msgid "Percentage of total sale order lines" @@ -164,6 +179,16 @@ msgstr "Classement par nombre de lignes de vente" msgid "Sale stock level history ids" msgstr "Historique des données de vente collectées" +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,help:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_sum_cumulated_percentages +msgid "Sum cumulated % so lines and cumulated % products" +msgstr "Somme %s cumulé lignes de vente et % cumulé articles" + +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_sum_cumulated_percentages +msgid "Sum of cummulated percentages" +msgstr "Somme % cumulés" + #. module: product_abc_classification_sale_stock #: model:ir.ui.view,arch_db:product_abc_classification_sale_stock.abc_sale_stock_level_history_search_view msgid "This Year" @@ -175,10 +200,15 @@ msgid "To date" msgstr "A" #. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_total_of_so_lines +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_total_so_lines msgid "Total of sale order lines" msgstr "Total des lignes de vente" +#. module: product_abc_classification_sale_stock +#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_total_products +msgid "Total products analysed" +msgstr "Total articles analysés" + #. module: product_abc_classification_sale_stock #: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_classification_profile_warehouse_id #: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_warehouse_id diff --git a/product_abc_classification_sale_stock/models/abc_classification_profile.py b/product_abc_classification_sale_stock/models/abc_classification_profile.py index 239058fcbcf2..c38641fc6e47 100644 --- a/product_abc_classification_sale_stock/models/abc_classification_profile.py +++ b/product_abc_classification_sale_stock/models/abc_classification_profile.py @@ -98,7 +98,7 @@ def _get_data(self, from_date=None): self.env.cr.execute( """ SELECT sol.product_id product_id, - COUNT(sol.id) number_of_so_lines + COUNT(sol.id) number_so_lines FROM sale_order so JOIN @@ -129,7 +129,7 @@ def _get_data(self, from_date=None): ) GROUP BY sol.product_id - ORDER BY number_of_so_lines DESC + ORDER BY number_so_lines DESC """, { "start_date": from_date, @@ -149,7 +149,7 @@ def _get_data(self, from_date=None): sale_stock_data = self._init_collected_data_instance() product_id = r[0] sale_stock_data.product = ProductProduct.browse(product_id) - sale_stock_data.number_of_so_lines = int(r[1]) + sale_stock_data.number_so_lines = int(r[1]) sale_stock_data.ranking = ranking sale_stock_data.from_date = from_date sale_stock_data.to_date = to_date @@ -162,7 +162,7 @@ def _get_data(self, from_date=None): for product_id in all_product_ids: sale_stock_data = self._init_collected_data_instance() sale_stock_data.product = ProductProduct.browse(product_id) - sale_stock_data.number_of_so_lines = 0 + sale_stock_data.number_so_lines = 0 sale_stock_data.ranking = ranking sale_stock_data.from_date = from_date sale_stock_data.to_date = to_date @@ -179,10 +179,10 @@ def _build_ordered_level_cumulative_percentage(self): levels = self.level_ids.sorted( key=attrgetter("percentage"), reverse=True ) - percentages = levels.mapped("percentage") cum_percentages = [] previous_percentage = None - for i, perc in enumerate(percentages): + for i, level in enumerate(levels): + perc = level.percentage + level.percentage_products if i == 0: percentage_to_append = perc cum_percentages.append(percentage_to_append) @@ -223,14 +223,14 @@ def _purge_obsolete_level_values(self, ids_to_remove): def _sale_stock_data_to_vals(self, sale_stock_data, create=False): self.ensure_one() - res = { - "computed_level_id": sale_stock_data.computed_level.id - } + res = {"computed_level_id": sale_stock_data.computed_level.id} if create: - res.update({ - "product_id": sale_stock_data.product.id, - "profile_id": sale_stock_data.profile.id, - }) + res.update( + { + "product_id": sale_stock_data.product.id, + "profile_id": sale_stock_data.profile.id, + } + ) return res @api.multi @@ -252,10 +252,22 @@ def _compute_abc_classification(self): ) level, percentage = level_percentage.pop(0) previous_data = {} + total_products = len(sale_stock_data_list) + percentage_products = 100.0 / total_products for i, sale_stock_data in enumerate(sale_stock_data_list): + sale_stock_data.total_products = total_products + sale_stock_data.percentage_products = percentage_products + sale_stock_data.cumulated_percentage_products = ( + sale_stock_data.percentage_products + if i == 0 + else ( + sale_stock_data.percentage_products + + previous_data.cumulated_percentage_products + ) + ) # Compute percentages and cumulative percentages for the products sale_stock_data.percentage = ( - (100.0 * sale_stock_data.number_of_so_lines / total) + (100.0 * sale_stock_data.number_so_lines / total) if total else 0.0 ) @@ -273,11 +285,16 @@ def _compute_abc_classification(self): _("Cumulative percentage greater than 100.") ) + sale_stock_data.sum_cumulated_percentages = ( + sale_stock_data.cumulated_percentage + + sale_stock_data.cumulated_percentage_products + ) + # Compute ABC classification for the products based on the - # cumulative percentage + # sum of cumulated percentages if ( - sale_stock_data.cumulated_percentage > percentage + sale_stock_data.sum_cumulated_percentages > percentage and len(level_percentage) > 0 ): level, percentage = level_percentage.pop(0) @@ -306,7 +323,7 @@ def _compute_abc_classification(self): product_abc_classification = ProductClassification.create( vals ) - sale_stock_data.total_of_so_lines = total + sale_stock_data.total_so_lines = total sale_stock_data.product_level = product_abc_classification previous_data = sale_stock_data self._log_history(sale_stock_data_list) @@ -339,10 +356,23 @@ class SaleStockData(object): insert these data into the abc.sale_stock.level.history table. """ + __slots__ = [ - "product", "profile", "computed_level", "ranking", "percentage", - "cumulated_percentage", "number_of_so_lines", "total_of_so_lines", - "product_level", "from_date", "to_date" + "product", + "profile", + "computed_level", + "ranking", + "percentage", + "cumulated_percentage", + "number_so_lines", + "total_so_lines", + "product_level", + "from_date", + "to_date", + "total_products", + "percentage_products", + "cumulated_percentage_products", + "sum_cumulated_percentages", ] def _to_csv_line(self): @@ -356,11 +386,15 @@ def _to_csv_line(self): self.ranking, self.percentage, self.cumulated_percentage, - self.number_of_so_lines, - self.total_of_so_lines, + self.number_so_lines, + self.total_so_lines, self.product_level.id, self.from_date, - self.to_date + self.to_date, + self.total_products, + self.percentage_products, + self.cumulated_percentage_products, + self.sum_cumulated_percentages, ] @classmethod @@ -379,9 +413,13 @@ def _get_col_names(cls): "ranking", "percentage", "cumulated_percentage", - "number_of_so_lines", - "total_of_so_lines", + "number_so_lines", + "total_so_lines", "product_level_id", "from_date", - "to_date" + "to_date", + "total_products", + "percentage_products", + "cumulated_percentage_products", + "sum_cumulated_percentages", ] diff --git a/product_abc_classification_sale_stock/models/abc_sale_stock_level_history.py b/product_abc_classification_sale_stock/models/abc_sale_stock_level_history.py index b0b66270ba08..301e089a643d 100644 --- a/product_abc_classification_sale_stock/models/abc_sale_stock_level_history.py +++ b/product_abc_classification_sale_stock/models/abc_sale_stock_level_history.py @@ -69,10 +69,10 @@ class AbcSaleStockLevelHistory(models.Model): readonly=True, help="Ranking by number of oder lines", ) - number_of_so_lines = fields.Integer( + number_so_lines = fields.Integer( "Number of sale order lines", required=True, readonly=True, ) - total_of_so_lines = fields.Integer( + total_so_lines = fields.Integer( "Total of sale order lines", required=True, readonly=True, ) percentage = fields.Float( @@ -80,12 +80,46 @@ class AbcSaleStockLevelHistory(models.Model): required=True, readonly=True, help="Percentage of total sale order lines", + digits=(7, 4), + group_operator="SUM", ) cumulated_percentage = fields.Float( "Cumulated percentage", required=True, readonly=True, help="Cumulated percentage of all the products with a better ranking", + digits=(7, 4), + group_operator=None, + ) + total_products = fields.Integer( + "Total products analysed", + required=True, + readonly=True, + group_operator=None, + ) + percentage_products = fields.Float( + "Percentage of products", + required=True, + readonly=True, + help="Percentage of total products analyzed", + digits=(7, 4), + group_operator="SUM", + ) + cumulated_percentage_products = fields.Float( + "Cumulated percentage of products", + required=True, + readonly=True, + help="Cumulated percentage of total products analyzed with a " + "better ranking", + digits=(7, 4), + group_operator=None, + ) + sum_cumulated_percentages = fields.Float( + "Sum of cummulated percentages", + required=True, + readonly=True, + help="Sum cumulated % so lines and cumulated % products", + group_operator=None, ) from_date = fields.Date(readonly=True) to_date = fields.Date(readonly=True) diff --git a/product_abc_classification_sale_stock/tests/test_abc_classification_profile.py b/product_abc_classification_sale_stock/tests/test_abc_classification_profile.py index faa97490184d..1789ac410850 100644 --- a/product_abc_classification_sale_stock/tests/test_abc_classification_profile.py +++ b/product_abc_classification_sale_stock/tests/test_abc_classification_profile.py @@ -241,9 +241,9 @@ def test_00(self): self.stock_profile._compute_abc_classification() self._assertLevelIs(self.product1, "a") self._assertLevelIs(self.product3, "a") - self._assertLevelIs(self.product5, "a") - self._assertLevelIs(self.product2, "b") + self._assertLevelIs(self.product5, "b") self._assertLevelIs(self.product6, "b") + self._assertLevelIs(self.product2, "c") self._assertLevelIs(self.product4, "c") self._assertLevelIs(self.product_new, "c") diff --git a/product_abc_classification_sale_stock/views/abc_classification_product_level.xml b/product_abc_classification_sale_stock/views/abc_classification_product_level.xml index e1a2afab086c..4c42652174a9 100644 --- a/product_abc_classification_sale_stock/views/abc_classification_product_level.xml +++ b/product_abc_classification_sale_stock/views/abc_classification_product_level.xml @@ -17,8 +17,8 @@ - - + + diff --git a/product_abc_classification_sale_stock/views/abc_sale_stock_level_history.xml b/product_abc_classification_sale_stock/views/abc_sale_stock_level_history.xml index c4a7413c1493..27b8e73e1d77 100644 --- a/product_abc_classification_sale_stock/views/abc_sale_stock_level_history.xml +++ b/product_abc_classification_sale_stock/views/abc_sale_stock_level_history.xml @@ -24,16 +24,18 @@ abc.sale_stock.level.history.tree (in product_abc_classification_sale_stock) abc.sale_stock.level.history - + - - + + + + From 3b6753470cb61698ea892b4bb53be8a229a7e76b Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Thu, 11 Mar 2021 15:52:44 +0100 Subject: [PATCH 06/21] [FIX] product_abc_classification_base: Fix bug in views --- .../views/abc_classification_product_level.xml | 2 +- product_abc_classification_base/views/product_product.xml | 2 +- product_abc_classification_base/views/product_template.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/product_abc_classification_base/views/abc_classification_product_level.xml b/product_abc_classification_base/views/abc_classification_product_level.xml index b6ffa789ea3f..7e770a76381f 100644 --- a/product_abc_classification_base/views/abc_classification_product_level.xml +++ b/product_abc_classification_base/views/abc_classification_product_level.xml @@ -16,7 +16,7 @@ - + diff --git a/product_abc_classification_base/views/product_product.xml b/product_abc_classification_base/views/product_product.xml index 8f8aa162366f..8640676151da 100644 --- a/product_abc_classification_base/views/product_product.xml +++ b/product_abc_classification_base/views/product_product.xml @@ -9,7 +9,7 @@ - {'default_product_id': active_id, 'default_profile_id': 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] 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_base/views/product_template.xml b/product_abc_classification_base/views/product_template.xml index 77f15cf33355..1162e400899b 100644 --- a/product_abc_classification_base/views/product_template.xml +++ b/product_abc_classification_base/views/product_template.xml @@ -14,7 +14,7 @@ Date: Tue, 25 Jan 2022 12:41:35 +0100 Subject: [PATCH 07/21] [IMP] Add flag to automatically set manual value of level_id to computed one --- .../abc_classification_product_level.py | 16 ++ .../models/abc_classification_profile.py | 2 + .../tests/common.py | 3 + .../test_abc_classification_product_level.py | 183 ++++++++++++++++++ .../views/abc_classification_profile.xml | 1 + .../models/__init__.py | 2 +- .../models/abc_classification_profile.py | 1 + 7 files changed, 207 insertions(+), 1 deletion(-) diff --git a/product_abc_classification_base/models/abc_classification_product_level.py b/product_abc_classification_base/models/abc_classification_product_level.py index f8d111e10930..abbcf4ea627b 100644 --- a/product_abc_classification_base/models/abc_classification_product_level.py +++ b/product_abc_classification_base/models/abc_classification_product_level.py @@ -140,4 +140,20 @@ def create(self, 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(AbcClassificationProductLevel, self).create(vals) + + 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(AbcClassificationProductLevel, self).write(values) diff --git a/product_abc_classification_base/models/abc_classification_profile.py b/product_abc_classification_base/models/abc_classification_profile.py index 90b7bc614c22..30c6f1e04f15 100644 --- a/product_abc_classification_base/models/abc_classification_profile.py +++ b/product_abc_classification_base/models/abc_classification_profile.py @@ -29,6 +29,8 @@ class AbcClassificationProfile(models.Model): required=True, ) + auto_apply_computed_value = fields.Boolean(default=False) + _sql_constraints = [ ("name_uniq", "UNIQUE(name)", _("Profile name must be unique")) ] diff --git a/product_abc_classification_base/tests/common.py b/product_abc_classification_base/tests/common.py index 823f564e69a9..548663fdd9b1 100644 --- a/product_abc_classification_base/tests/common.py +++ b/product_abc_classification_base/tests/common.py @@ -82,6 +82,9 @@ def setUpClass(cls): 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( diff --git a/product_abc_classification_base/tests/test_abc_classification_product_level.py b/product_abc_classification_base/tests/test_abc_classification_product_level.py index e3babcecbe1b..5581dd6f166b 100644 --- a/product_abc_classification_base/tests/test_abc_classification_product_level.py +++ b/product_abc_classification_base/tests/test_abc_classification_product_level.py @@ -171,3 +171,186 @@ def test_05(self): "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): + product_2 = self.env["product.product"].create( + { + "name": "Test 2", + "uom_id": self.uom_unit.id, + "uom_po_id": self.uom_unit.id, + } + ) + + product_3 = self.env["product.product"].create( + { + "name": "Test 3", + "uom_id": self.uom_unit.id, + "uom_po_id": self.uom_unit.id, + } + ) + self.ProductLevel.create( + { + "product_id": product_2.id, + "manual_level_id": self.classification_level_b.id, + "computed_level_id": self.classification_level_a.id, + "profile_id": self.classification_profile.id + } + ) + self.ProductLevel.create( + { + "product_id": product_3.id, + "manual_level_id": self.classification_level_b.id, + "computed_level_id": self.classification_level_a.id, + "profile_id": self.classification_profile.id + } + ) + 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): + product_2 = self.env["product.product"].create( + { + "name": "Test 2", + "uom_id": self.uom_unit.id, + "uom_po_id": self.uom_unit.id, + } + ) + + product_3 = self.env["product.product"].create( + { + "name": "Test 3", + "uom_id": self.uom_unit.id, + "uom_po_id": self.uom_unit.id, + } + ) + self.ProductLevel.create( + { + "product_id": product_2.id, + "manual_level_id": self.classification_level_b.id, + "computed_level_id": self.classification_level_a.id, + "profile_id": self.classification_profile.id + } + ) + self.ProductLevel.create( + { + "product_id": product_3.id, + "manual_level_id": self.classification_level_b.id, + "computed_level_id": self.classification_level_a.id, + "profile_id": self.classification_profile.id + } + ) + 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) diff --git a/product_abc_classification_base/views/abc_classification_profile.xml b/product_abc_classification_base/views/abc_classification_profile.xml index 1831db224cdb..d51cdca7d411 100644 --- a/product_abc_classification_base/views/abc_classification_profile.xml +++ b/product_abc_classification_base/views/abc_classification_profile.xml @@ -21,6 +21,7 @@ + diff --git a/product_abc_classification_sale_stock/models/__init__.py b/product_abc_classification_sale_stock/models/__init__.py index ba6399b8dd63..d64de14ddd6c 100644 --- a/product_abc_classification_sale_stock/models/__init__.py +++ b/product_abc_classification_sale_stock/models/__init__.py @@ -1,3 +1,3 @@ from . import abc_classification_profile -from . import abc_sale_stock_level_history from . import abc_classification_product_level +from . import abc_sale_stock_level_history diff --git a/product_abc_classification_sale_stock/models/abc_classification_profile.py b/product_abc_classification_sale_stock/models/abc_classification_profile.py index c38641fc6e47..b635836e3029 100644 --- a/product_abc_classification_sale_stock/models/abc_classification_profile.py +++ b/product_abc_classification_sale_stock/models/abc_classification_profile.py @@ -229,6 +229,7 @@ def _sale_stock_data_to_vals(self, sale_stock_data, create=False): { "product_id": sale_stock_data.product.id, "profile_id": sale_stock_data.profile.id, + "auto_apply_computed_value": sale_stock_data.profile.auto_apply_computed_value } ) return res From 487037902a9721c16e0c1b9863716b371b0cc1a5 Mon Sep 17 00:00:00 2001 From: Lindsay Date: Fri, 28 Jan 2022 14:47:25 +0100 Subject: [PATCH 08/21] [FIX] Trigger the auto compute if flag changes but not the computed_value --- .../abc_classification_product_level.py | 1 + .../models/abc_classification_profile.py | 24 ++++ .../test_abc_classification_product_level.py | 126 +++++++++--------- .../tests/test_abc_classification_profile.py | 1 + .../models/abc_classification_profile.py | 1 - .../tests/test_abc_classification_profile.py | 1 + 6 files changed, 91 insertions(+), 63 deletions(-) diff --git a/product_abc_classification_base/models/abc_classification_product_level.py b/product_abc_classification_base/models/abc_classification_product_level.py index abbcf4ea627b..e6bd0eae043c 100644 --- a/product_abc_classification_base/models/abc_classification_product_level.py +++ b/product_abc_classification_base/models/abc_classification_product_level.py @@ -157,3 +157,4 @@ def write(self, vals): if profile.auto_apply_computed_value and "computed_level_id" in values: values["manual_level_id"] = values["computed_level_id"] return super(AbcClassificationProductLevel, self).write(values) + diff --git a/product_abc_classification_base/models/abc_classification_profile.py b/product_abc_classification_base/models/abc_classification_profile.py index 30c6f1e04f15..632d8a04f94c 100644 --- a/product_abc_classification_base/models/abc_classification_profile.py +++ b/product_abc_classification_base/models/abc_classification_profile.py @@ -5,6 +5,7 @@ from odoo import api, fields, models, _ from odoo.exceptions import ValidationError +from psycopg2.extensions import AsIs class AbcClassificationProfile(models.Model): @@ -72,3 +73,26 @@ def _compute_abc_classification(self): @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): + for rec in self: + self.env.cr.execute(""" + UPDATE %(table)s + SET manual_level_id = computed_level_id, + level_id = computed_level_id + WHERE profile_id = %(profile_id)s + + """, {"table": AsIs(self.env["abc.classification.product.level"]._table), + "profile_id": rec.id} + ) + + self.env["abc.classification.product.level"].invalidate_cache( + ["manual_level_id", "computed_level_id", "level_id"] + ) + diff --git a/product_abc_classification_base/tests/test_abc_classification_product_level.py b/product_abc_classification_base/tests/test_abc_classification_product_level.py index 5581dd6f166b..c2db369bf51b 100644 --- a/product_abc_classification_base/tests/test_abc_classification_product_level.py +++ b/product_abc_classification_base/tests/test_abc_classification_product_level.py @@ -27,6 +27,40 @@ def setUpClass(cls): } ) + @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: @@ -248,37 +282,7 @@ def test_07_update_product_level_without_auto_compute(self): ) def test_08_update_recordset_with__autocompute(self): - product_2 = self.env["product.product"].create( - { - "name": "Test 2", - "uom_id": self.uom_unit.id, - "uom_po_id": self.uom_unit.id, - } - ) - - product_3 = self.env["product.product"].create( - { - "name": "Test 3", - "uom_id": self.uom_unit.id, - "uom_po_id": self.uom_unit.id, - } - ) - self.ProductLevel.create( - { - "product_id": product_2.id, - "manual_level_id": self.classification_level_b.id, - "computed_level_id": self.classification_level_a.id, - "profile_id": self.classification_profile.id - } - ) - self.ProductLevel.create( - { - "product_id": product_3.id, - "manual_level_id": self.classification_level_b.id, - "computed_level_id": self.classification_level_a.id, - "profile_id": self.classification_profile.id - } - ) + self._create_product_levels() self.classification_profile.auto_apply_computed_value = True levels = self.ProductLevel.search([("profile_id", "=", self.classification_profile.id)]) @@ -294,37 +298,7 @@ def test_08_update_recordset_with__autocompute(self): self.assertEqual(level.level_id, self.classification_level_b) def test_09_update_recordset_and_change_profile(self): - product_2 = self.env["product.product"].create( - { - "name": "Test 2", - "uom_id": self.uom_unit.id, - "uom_po_id": self.uom_unit.id, - } - ) - - product_3 = self.env["product.product"].create( - { - "name": "Test 3", - "uom_id": self.uom_unit.id, - "uom_po_id": self.uom_unit.id, - } - ) - self.ProductLevel.create( - { - "product_id": product_2.id, - "manual_level_id": self.classification_level_b.id, - "computed_level_id": self.classification_level_a.id, - "profile_id": self.classification_profile.id - } - ) - self.ProductLevel.create( - { - "product_id": product_3.id, - "manual_level_id": self.classification_level_b.id, - "computed_level_id": self.classification_level_a.id, - "profile_id": self.classification_profile.id - } - ) + self._create_product_levels() self.classification_profile_bis.auto_apply_computed_value = True levels = self.ProductLevel.search([("profile_id", "=", self.classification_profile.id)]) @@ -354,3 +328,31 @@ def test_10_create_product_level_for_profile_auto_assign(self): 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_base/tests/test_abc_classification_profile.py b/product_abc_classification_base/tests/test_abc_classification_profile.py index fd6c6a6bac71..771785bbd16d 100644 --- a/product_abc_classification_base/tests/test_abc_classification_profile.py +++ b/product_abc_classification_base/tests/test_abc_classification_profile.py @@ -298,3 +298,4 @@ def test_07(self): "profile_type": "test_type", } ) + diff --git a/product_abc_classification_sale_stock/models/abc_classification_profile.py b/product_abc_classification_sale_stock/models/abc_classification_profile.py index b635836e3029..c38641fc6e47 100644 --- a/product_abc_classification_sale_stock/models/abc_classification_profile.py +++ b/product_abc_classification_sale_stock/models/abc_classification_profile.py @@ -229,7 +229,6 @@ def _sale_stock_data_to_vals(self, sale_stock_data, create=False): { "product_id": sale_stock_data.product.id, "profile_id": sale_stock_data.profile.id, - "auto_apply_computed_value": sale_stock_data.profile.auto_apply_computed_value } ) return res diff --git a/product_abc_classification_sale_stock/tests/test_abc_classification_profile.py b/product_abc_classification_sale_stock/tests/test_abc_classification_profile.py index 1789ac410850..6976a0345999 100644 --- a/product_abc_classification_sale_stock/tests/test_abc_classification_profile.py +++ b/product_abc_classification_sale_stock/tests/test_abc_classification_profile.py @@ -275,3 +275,4 @@ def test_02(self): self.assertEqual(len(levels.sale_stock_level_history_ids), 1) self.stock_profile._compute_abc_classification() self.assertEqual(len(levels.sale_stock_level_history_ids), 2) + From 02ca4dc41f50d4ca936c89df46bc6f4a3e5ae2e3 Mon Sep 17 00:00:00 2001 From: Lindsay Date: Mon, 31 Jan 2022 11:24:45 +0100 Subject: [PATCH 09/21] [FIX] Set flag to false if manual = computed --- product_abc_classification_base/i18n/fr.po | 5 +++++ .../models/abc_classification_profile.py | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/product_abc_classification_base/i18n/fr.po b/product_abc_classification_base/i18n/fr.po index 4baea279b742..b311e545b6ea 100644 --- a/product_abc_classification_base/i18n/fr.po +++ b/product_abc_classification_base/i18n/fr.po @@ -316,6 +316,11 @@ msgstr "La somme des pourcentages d'articles ne doit pas dépasser 100." msgid "Type of ABC classification" msgstr "Type de classification ABC" +#. module: product_abc_classification_base +#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_auto_apply_computed_value +msgid "Auto apply computed value" +msgstr "Appliquer automatiquement la classification calculée" + #. module: product_abc_classification_base #: model:ir.model,name:product_abc_classification_base.model_abc_classification_level msgid "abc.classification.level" diff --git a/product_abc_classification_base/models/abc_classification_profile.py b/product_abc_classification_base/models/abc_classification_profile.py index 632d8a04f94c..3ec55773eba1 100644 --- a/product_abc_classification_base/models/abc_classification_profile.py +++ b/product_abc_classification_base/models/abc_classification_profile.py @@ -85,7 +85,8 @@ def _auto_apply_computed_value_for_product_levels(self): self.env.cr.execute(""" UPDATE %(table)s SET manual_level_id = computed_level_id, - level_id = computed_level_id + level_id = computed_level_id, + flag = false WHERE profile_id = %(profile_id)s """, {"table": AsIs(self.env["abc.classification.product.level"]._table), From 0a01fcec1bcb423925a3be7d96e0ba2317b981f7 Mon Sep 17 00:00:00 2001 From: Lindsay Date: Mon, 7 Feb 2022 09:10:06 +0100 Subject: [PATCH 10/21] [FIX] Propagate recompute level info to all computed values (abc storage on product, level_id, flag, ...) --- .../models/abc_classification_profile.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/product_abc_classification_base/models/abc_classification_profile.py b/product_abc_classification_base/models/abc_classification_profile.py index 3ec55773eba1..6e99540e5fa6 100644 --- a/product_abc_classification_base/models/abc_classification_profile.py +++ b/product_abc_classification_base/models/abc_classification_profile.py @@ -81,19 +81,20 @@ def write(self, vals): 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, - level_id = computed_level_id, - flag = false + 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} ) - - self.env["abc.classification.product.level"].invalidate_cache( - ["manual_level_id", "computed_level_id", "level_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() From 99edc77c4f25300fcf676ea0d44fa2f7ff2858b1 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Thu, 10 Nov 2022 08:48:46 +0100 Subject: [PATCH 11/21] [IMP] product_abc_classification_base: pre-commit stuff --- .../__manifest__.py | 1 - .../models/abc_classification_level.py | 21 +- .../abc_classification_product_level.py | 26 +- .../models/abc_classification_profile.py | 39 +- .../models/product_product.py | 1 - .../models/product_template.py | 10 +- .../tests/common.py | 9 +- .../test_abc_classification_product_level.py | 157 ++++--- .../tests/test_abc_classification_profile.py | 5 +- .../tests/test_product.py | 16 +- .../abc_classification_product_level.xml | 89 ++-- .../views/abc_classification_profile.xml | 24 +- .../views/product_product.xml | 7 +- .../views/product_template.xml | 26 +- .../README.rst | 87 ---- .../__init__.py | 1 - .../__manifest__.py | 24 - .../demo/abc_classification_level.xml | 20 - .../demo/abc_classification_profile.xml | 12 - .../i18n/fr.po | 222 --------- .../models/__init__.py | 3 - .../abc_classification_product_level.py | 15 - .../models/abc_classification_profile.py | 425 ----------------- .../models/abc_sale_stock_level_history.py | 125 ----- .../readme/CONTRIBUTORS.rst | 2 - .../readme/DESCRIPTION.rst | 2 - .../readme/USAGE.rst | 8 - .../security/abc_sale_stock_level_history.xml | 18 - .../static/description/icon.png | Bin 9455 -> 0 bytes .../static/description/index.html | 431 ------------------ .../tests/__init__.py | 1 - .../tests/test_abc_classification_profile.py | 278 ----------- .../abc_classification_product_level.xml | 30 -- .../views/abc_classification_profile.xml | 15 - .../views/abc_sale_stock_level_history.xml | 60 --- .../.eggs/README.txt | 6 - .../odoo/__init__.py | 1 - .../odoo/addons/__init__.py | 1 - .../product_abc_classification_sale_stock | 1 - .../setup.py | 6 - 40 files changed, 221 insertions(+), 2004 deletions(-) delete mode 100644 product_abc_classification_sale_stock/README.rst delete mode 100644 product_abc_classification_sale_stock/__init__.py delete mode 100644 product_abc_classification_sale_stock/__manifest__.py delete mode 100644 product_abc_classification_sale_stock/demo/abc_classification_level.xml delete mode 100644 product_abc_classification_sale_stock/demo/abc_classification_profile.xml delete mode 100644 product_abc_classification_sale_stock/i18n/fr.po delete mode 100644 product_abc_classification_sale_stock/models/__init__.py delete mode 100644 product_abc_classification_sale_stock/models/abc_classification_product_level.py delete mode 100644 product_abc_classification_sale_stock/models/abc_classification_profile.py delete mode 100644 product_abc_classification_sale_stock/models/abc_sale_stock_level_history.py delete mode 100644 product_abc_classification_sale_stock/readme/CONTRIBUTORS.rst delete mode 100644 product_abc_classification_sale_stock/readme/DESCRIPTION.rst delete mode 100644 product_abc_classification_sale_stock/readme/USAGE.rst delete mode 100644 product_abc_classification_sale_stock/security/abc_sale_stock_level_history.xml delete mode 100644 product_abc_classification_sale_stock/static/description/icon.png delete mode 100644 product_abc_classification_sale_stock/static/description/index.html delete mode 100644 product_abc_classification_sale_stock/tests/__init__.py delete mode 100644 product_abc_classification_sale_stock/tests/test_abc_classification_profile.py delete mode 100644 product_abc_classification_sale_stock/views/abc_classification_product_level.xml delete mode 100644 product_abc_classification_sale_stock/views/abc_classification_profile.xml delete mode 100644 product_abc_classification_sale_stock/views/abc_sale_stock_level_history.xml delete mode 100644 setup/product_abc_classification_sale_stock/.eggs/README.txt delete mode 100644 setup/product_abc_classification_sale_stock/odoo/__init__.py delete mode 100644 setup/product_abc_classification_sale_stock/odoo/addons/__init__.py delete mode 120000 setup/product_abc_classification_sale_stock/odoo/addons/product_abc_classification_sale_stock delete mode 100644 setup/product_abc_classification_sale_stock/setup.py diff --git a/product_abc_classification_base/__manifest__.py b/product_abc_classification_base/__manifest__.py index 7ae04a9b7d73..f0f46384aa18 100644 --- a/product_abc_classification_base/__manifest__.py +++ b/product_abc_classification_base/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 ForgeFlow # Copyright 2021 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). diff --git a/product_abc_classification_base/models/abc_classification_level.py b/product_abc_classification_base/models/abc_classification_level.py index 49ff40086fab..5144c5bb07ad 100644 --- a/product_abc_classification_base/models/abc_classification_level.py +++ b/product_abc_classification_base/models/abc_classification_level.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 ForgeFlow # Copyright 2021 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -13,13 +12,9 @@ class AbcClassificationLevel(models.Model): _order = "percentage desc, id desc" _rec_name = "name" - percentage_products = fields.Float( - default=0.0, required=True, string="% Products" - ) + 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" - ) + profile_id = fields.Many2one("abc.classification.profile", ondelete="cascade") name = fields.Char(help="Classification A, B or C", required=True) @@ -35,13 +30,9 @@ class AbcClassificationLevel(models.Model): def _check_percentage(self): for level in self: if level.percentage > 100.0: - raise ValidationError( - _("The percentage cannot be greater than 100.") - ) + raise ValidationError(_("The percentage cannot be greater than 100.")) if level.percentage <= 0.0: - raise ValidationError( - _("The percentage should be a positive number.") - ) + raise ValidationError(_("The percentage should be a positive number.")) @api.constrains("percentage_products") def _check_percentage_products(self): @@ -52,7 +43,5 @@ def _check_percentage_products(self): ) if level.percentage_products <= 0.0: raise ValidationError( - _( - "The percentage of products should be a positive number." - ) + _("The percentage of products should be a positive number.") ) diff --git a/product_abc_classification_base/models/abc_classification_product_level.py b/product_abc_classification_base/models/abc_classification_product_level.py index e6bd0eae043c..55690ec1bc69 100644 --- a/product_abc_classification_base/models/abc_classification_product_level.py +++ b/product_abc_classification_base/models/abc_classification_product_level.py @@ -1,8 +1,7 @@ -# -*- coding: utf-8 -*- # 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 import _, api, fields, models from odoo.exceptions import ValidationError @@ -18,7 +17,7 @@ class AbcClassificationProductLevel(models.Model): "abc.classification.level", string="Manual classification level", track_visibility="onchange", - domain="[('profile_id', '=', profile_id)]" + domain="[('profile_id', '=', profile_id)]", ) computed_level_id = fields.Many2one( "abc.classification.level", @@ -30,7 +29,7 @@ class AbcClassificationProductLevel(models.Model): string="Classification level", compute="_compute_level_id", store=True, - domain="[('profile_id', '=', profile_id)]" + domain="[('profile_id', '=', profile_id)]", ) flag = fields.Boolean( default=False, @@ -66,7 +65,7 @@ class AbcClassificationProductLevel(models.Model): ) allowed_profile_ids = fields.Many2many( comodel_name="abc.classification.profile", - related="product_id.abc_classification_profile_ids" + related="product_id.abc_classification_profile_ids", ) _sql_constraints = [ @@ -92,10 +91,7 @@ def _check_level(self): "profile as the one on the product level" ) ) - if ( - rec.manual_level_id - and rec.manual_level_id.profile_id != rec.profile_id - ): + 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 " @@ -113,7 +109,7 @@ def _onchange_product_tmpl_id(self): @api.depends("level_id", "profile_id") def _compute_display_name(self): for record in self: - record.display_name = u"{profile_name}: {level_name}".format( + record.display_name = "{profile_name}: {level_name}".format( profile_name=record.profile_id.name, level_name=record.level_id.name, ) @@ -130,8 +126,7 @@ def _compute_level_id(self): def _compute_flag(self): for rec in self: rec.flag = ( - rec.computed_level_id - and rec.manual_level_id != rec.computed_level_id + rec.computed_level_id and rec.manual_level_id != rec.computed_level_id ) @api.model @@ -142,7 +137,7 @@ def create(self, vals): vals["manual_level_id"] = vals["computed_level_id"] if "profile_id" in vals: - profile = self.env["abc.classification.profile"].browse(vals['profile_id']) + 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(AbcClassificationProductLevel, self).create(vals) @@ -150,11 +145,12 @@ def create(self, vals): def write(self, vals): values = vals.copy() if "profile_id" in values: - profile = self.env["abc.classification.profile"].browse(values['profile_id']) + 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(AbcClassificationProductLevel, self).write(values) - diff --git a/product_abc_classification_base/models/abc_classification_profile.py b/product_abc_classification_base/models/abc_classification_profile.py index 6e99540e5fa6..1c669d4a0ca1 100644 --- a/product_abc_classification_base/models/abc_classification_profile.py +++ b/product_abc_classification_base/models/abc_classification_profile.py @@ -1,12 +1,12 @@ -# -*- coding: utf-8 -*- # 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 from psycopg2.extensions import AsIs +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + class AbcClassificationProfile(models.Model): @@ -32,9 +32,7 @@ class AbcClassificationProfile(models.Model): auto_apply_computed_value = fields.Boolean(default=False) - _sql_constraints = [ - ("name_uniq", "UNIQUE(name)", _("Profile name must be unique")) - ] + _sql_constraints = [("name_uniq", "UNIQUE(name)", _("Profile name must be unique"))] @api.constrains("level_ids") def _check_levels(self): @@ -43,20 +41,13 @@ def _check_levels(self): total = sum(percentages) if profile.level_ids and total != 100.0: raise ValidationError( - _( - "The sum of the percentages of the levels should be " - "100." - ) + _("The sum of the percentages of the levels should be " "100.") ) - if profile.level_ids and len({}.fromkeys(percentages)) != len( - percentages - ): + 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" - ) + percentage_productss = profile.level_ids.mapped("percentage_products") total = sum(percentage_productss) if profile.level_ids and total != 100.0: raise ValidationError( @@ -76,24 +67,30 @@ def _cron_compute_abc_classification(self): def write(self, vals): res = super(AbcClassificationProfile, self).write(vals) - if 'auto_apply_computed_value' in vals and vals['auto_apply_computed_value']: + 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(""" + 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} + """, + { + "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) + 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"]) diff --git a/product_abc_classification_base/models/product_product.py b/product_abc_classification_base/models/product_product.py index ad0c441ab5c5..80f1fc1513f3 100644 --- a/product_abc_classification_base/models/product_product.py +++ b/product_abc_classification_base/models/product_product.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 ForgeFlow # Copyright 2021 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). diff --git a/product_abc_classification_base/models/product_template.py b/product_abc_classification_base/models/product_template.py index 6eff21320142..48c01bbc47d0 100644 --- a/product_abc_classification_base/models/product_template.py +++ b/product_abc_classification_base/models/product_template.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 ForgeFlow # Copyright 2021 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -48,8 +47,9 @@ def _compute_abc_classification_product_level_ids(self): ) for template in unique_variants: variants = template.product_variant_ids - template.abc_classification_product_level_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 @@ -57,12 +57,14 @@ 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 = \ + 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 = \ + variants.abc_classification_product_level_ids = ( template.abc_classification_product_level_ids + ) diff --git a/product_abc_classification_base/tests/common.py b/product_abc_classification_base/tests/common.py index 548663fdd9b1..f62e9dd2a41e 100644 --- a/product_abc_classification_base/tests/common.py +++ b/product_abc_classification_base/tests/common.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -78,13 +77,9 @@ def setUpClass(cls): } ) levels = cls.classification_profile_bis.level_ids - cls.classification_level_bis_a = levels.filtered( - lambda l: l.name == "a" - ) + cls.classification_level_bis_a = levels.filtered(lambda l: l.name == "a") - cls.classification_level_bis_b = levels.filtered( - lambda l: l.name == "b" - ) + 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( diff --git a/product_abc_classification_base/tests/test_abc_classification_product_level.py b/product_abc_classification_base/tests/test_abc_classification_product_level.py index c2db369bf51b..fe87c93675ec 100644 --- a/product_abc_classification_base/tests/test_abc_classification_product_level.py +++ b/product_abc_classification_base/tests/test_abc_classification_product_level.py @@ -1,12 +1,12 @@ -# -*- coding: utf-8 -*- # Copyright 2021 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from psycopg2 import IntegrityError -from .common import ABCClassificationLevelCase from odoo.exceptions import ValidationError +from .common import ABCClassificationLevelCase + class TestABCClassificationProductLevel(ABCClassificationLevelCase): @classmethod @@ -23,7 +23,7 @@ def setUpClass(cls): { "product_id": cls.product_product.id, "computed_level_id": cls.classification_level_a.id, - "profile_id": cls.classification_profile.id + "profile_id": cls.classification_profile.id, } ) @@ -49,7 +49,7 @@ def _create_product_levels(cls): "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 + "profile_id": cls.classification_profile.id, } ) cls.ProductLevel.create( @@ -57,7 +57,7 @@ def _create_product_levels(cls): "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 + "profile_id": cls.classification_profile.id, } ) @@ -75,7 +75,7 @@ def test_00(self): { "product_id": self.product_1.id, "computed_level_id": self.classification_level_a.id, - "profile_id": self.classification_profile.id + "profile_id": self.classification_profile.id, } ) self.assertEqual(level.manual_level_id, self.classification_level_a) @@ -99,7 +99,7 @@ def test_01(self): { "product_id": self.product_1.id, "manual_level_id": self.classification_level_a.id, - "profile_id": self.classification_profile.id + "profile_id": self.classification_profile.id, } ) self.assertFalse(level.computed_level_id) @@ -127,22 +127,14 @@ def test_02(self): self.assertEqual( self.product_level.computed_level_id, self.classification_level_a ) - self.assertEqual( - self.product_level.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.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.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): @@ -159,7 +151,7 @@ def test_03(self): { "product_id": self.product_product.id, "computed_level_id": self.classification_level_a.id, - "profile_id": self.classification_profile.id + "profile_id": self.classification_profile.id, } ) @@ -175,20 +167,26 @@ def test_04(self): 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 - }) + 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({ + 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_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 - }) + "computed_level_id": self.classification_level_bis_a.id, + "profile_id": self.classification_profile_bis.id, + } + ) def test_05(self): """ @@ -202,16 +200,18 @@ def test_05(self): self.ProductLevel.create( { "product_id": self.product_1.id, - "profile_id": self.classification_profile.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.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, @@ -220,13 +220,13 @@ def test_06_update_product_level_with_auto_compute(self): 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.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.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, @@ -234,17 +234,17 @@ def test_06_update_product_level_with_auto_compute(self): 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 - ) + 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.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, @@ -256,16 +256,14 @@ def test_07_update_product_level_without_auto_compute(self): 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.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, @@ -277,19 +275,21 @@ def test_07_update_product_level_without_auto_compute(self): self.assertEqual( self.product_level.manual_level_id, self.classification_level_a ) - self.assertEqual( - self.product_level.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, - }) + 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) @@ -301,12 +301,15 @@ 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 - - }) + 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) @@ -321,7 +324,7 @@ def test_10_create_product_level_for_profile_auto_assign(self): "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 + "profile_id": self.classification_profile.id, } ) self.assertEqual(level.manual_level_id, level.computed_level_id) @@ -332,7 +335,9 @@ def test_10_create_product_level_for_profile_auto_assign(self): def test_11_auto_apply_computed_level(self): self._create_product_levels() - levels = self.ProductLevel.search([("profile_id", "=", self.classification_profile.id)]) + levels = self.ProductLevel.search( + [("profile_id", "=", self.classification_profile.id)] + ) level0 = levels[0] level1 = levels[1] level2 = levels[2] diff --git a/product_abc_classification_base/tests/test_abc_classification_profile.py b/product_abc_classification_base/tests/test_abc_classification_profile.py index 771785bbd16d..51691913cf2b 100644 --- a/product_abc_classification_base/tests/test_abc_classification_profile.py +++ b/product_abc_classification_base/tests/test_abc_classification_profile.py @@ -1,12 +1,12 @@ -# -*- coding: utf-8 -*- # Copyright 2021 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from psycopg2 import IntegrityError -from .common import ABCClassificationCase from odoo.exceptions import ValidationError +from .common import ABCClassificationCase + class TestABCClassificationProfile(ABCClassificationCase): def test_00(self): @@ -298,4 +298,3 @@ def test_07(self): "profile_type": "test_type", } ) - diff --git a/product_abc_classification_base/tests/test_product.py b/product_abc_classification_base/tests/test_product.py index 706c86d1d215..ed3406fcfaa4 100644 --- a/product_abc_classification_base/tests/test_product.py +++ b/product_abc_classification_base/tests/test_product.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -81,12 +80,8 @@ def test_02(self): ) product_level.unlink() - self.assertFalse( - self.product_product.abc_classification_product_level_ids - ) - self.assertFalse( - self.product_template.abc_classification_product_level_ids - ) + self.assertFalse(self.product_product.abc_classification_product_level_ids) + self.assertFalse(self.product_template.abc_classification_product_level_ids) def test_03(self): """ @@ -108,8 +103,7 @@ def test_03(self): } ) self.assertEqual( - new_variant.abc_classification_product_level_ids, product_level, - ) - self.assertFalse( - self.product_template.abc_classification_product_level_ids + 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_base/views/abc_classification_product_level.xml b/product_abc_classification_base/views/abc_classification_product_level.xml index 7e770a76381f..fed469a26041 100644 --- a/product_abc_classification_base/views/abc_classification_product_level.xml +++ b/product_abc_classification_base/views/abc_classification_product_level.xml @@ -3,62 +3,85 @@ License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). --> - abc.classification.product.level.form (in product_abc_classification_base) + abc.classification.product.level.form (in product_abc_classification_base) abc.classification.product.level
+ class="alert alert-danger" + role="alert" + attrs="{'invisible': [('flag','=',False)]}" + >Computed level differs from the specified level
- - - - - - - - - + + + + + + + + +
- - + +
- abc.classification.product.level.tree (in product_abc_classification_base) + abc.classification.product.level.tree (in product_abc_classification_base) abc.classification.product.level - + - - + + - abc.classification.product.level.search (in product_abc_classification_base) + abc.classification.product.level.search (in product_abc_classification_base) abc.classification.product.level - - - - - + + + + + - - + + @@ -70,9 +93,9 @@ {'search_default_group_by_level': 1} + action="abc_classification_product_level_action" + id="menu_abc_classification_product_level_config_stock" + parent="stock.menu_stock_inventory_control" + sequence="99" + />
diff --git a/product_abc_classification_base/views/abc_classification_profile.xml b/product_abc_classification_base/views/abc_classification_profile.xml index d51cdca7d411..cca0af065376 100644 --- a/product_abc_classification_base/views/abc_classification_profile.xml +++ b/product_abc_classification_base/views/abc_classification_profile.xml @@ -3,7 +3,9 @@ License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). --> - abc.classification.profile.form (in product_abc_classification_base) + abc.classification.profile.form (in product_abc_classification_base) abc.classification.profile
@@ -12,9 +14,9 @@ - - - + + + @@ -28,7 +30,9 @@ - abc.classification.profile.tree (in product_abc_classification_base) + abc.classification.profile.tree (in product_abc_classification_base) abc.classification.profile @@ -42,9 +46,9 @@ tree,form + action="abc_classification_profile_action" + id="menu_abc_classification_profile_config_stock" + parent="stock.menu_product_in_config_stock" + sequence="10" + /> diff --git a/product_abc_classification_base/views/product_product.xml b/product_abc_classification_base/views/product_product.xml index 8640676151da..ef810e32598a 100644 --- a/product_abc_classification_base/views/product_product.xml +++ b/product_abc_classification_base/views/product_product.xml @@ -7,9 +7,10 @@ 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} + + {'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_base/views/product_template.xml b/product_abc_classification_base/views/product_template.xml index 1162e400899b..3e49b0a2ab79 100644 --- a/product_abc_classification_base/views/product_template.xml +++ b/product_abc_classification_base/views/product_template.xml @@ -8,18 +8,26 @@ - + - + name="abc_classification_product_level_ids" + widget="many2many_tags" + context="{'default_product_tmpl_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}" + options="{'open': true}" + attrs="{'readonly': [('product_variant_count', '>', 1)]}" + domain="[('product_tmpl_id', '=', active_id)]" + /> + diff --git a/product_abc_classification_sale_stock/README.rst b/product_abc_classification_sale_stock/README.rst deleted file mode 100644 index 461907d96c63..000000000000 --- a/product_abc_classification_sale_stock/README.rst +++ /dev/null @@ -1,87 +0,0 @@ -====================================================== -Product Abc Classification based on delivered products -====================================================== - -.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! 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/10.0/product_abc_classification_sale_stock - :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-10-0/product-attribute-10-0-product_abc_classification_sale_stock - :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/10.0 - :alt: Try me on Runbot - -|badge1| |badge2| |badge3| |badge4| |badge5| - -This modules includes an ABC analysis computation profile based -on the number of sale order lines delivered from a given date 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 variants, and assign them a profile. -Then the cron classification will proceed to assign to these products one of the profile's levels. - -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 - -Contributors -~~~~~~~~~~~~ - -* Lindsay Marion -* Laurent Mignon - -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_sale_stock/__init__.py b/product_abc_classification_sale_stock/__init__.py deleted file mode 100644 index 0650744f6bc6..000000000000 --- a/product_abc_classification_sale_stock/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import models diff --git a/product_abc_classification_sale_stock/__manifest__.py b/product_abc_classification_sale_stock/__manifest__.py deleted file mode 100644 index dff75041bed5..000000000000 --- a/product_abc_classification_sale_stock/__manifest__.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2021 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -{ - "name": "Product Abc Classification based on delivered products", - "summary": """ - Compute ABC classification from the number of delivered sale order - line by product""", - "version": "10.0.1.0.0", - "license": "AGPL-3", - "author": "ACSONE SA/NV,Odoo Community Association (OCA)", - "depends": ["product_abc_classification_base", "sale_stock"], - "data": [ - "views/abc_classification_product_level.xml", - "security/abc_sale_stock_level_history.xml", - "views/abc_sale_stock_level_history.xml", - "views/abc_classification_profile.xml", - ], - "demo": [ - "demo/abc_classification_level.xml", - "demo/abc_classification_profile.xml", - ], -} diff --git a/product_abc_classification_sale_stock/demo/abc_classification_level.xml b/product_abc_classification_sale_stock/demo/abc_classification_level.xml deleted file mode 100644 index 6777e316af8e..000000000000 --- a/product_abc_classification_sale_stock/demo/abc_classification_level.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - a - 80 - 20 - - - b - 15 - 30 - - - c - 5 - 50 - - diff --git a/product_abc_classification_sale_stock/demo/abc_classification_profile.xml b/product_abc_classification_sale_stock/demo/abc_classification_profile.xml deleted file mode 100644 index aee51147dcb0..000000000000 --- a/product_abc_classification_sale_stock/demo/abc_classification_profile.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - Sale stock profile - sale_stock - - 365 - - - diff --git a/product_abc_classification_sale_stock/i18n/fr.po b/product_abc_classification_sale_stock/i18n/fr.po deleted file mode 100644 index 5c53a3d8b9fd..000000000000 --- a/product_abc_classification_sale_stock/i18n/fr.po +++ /dev/null @@ -1,222 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * product_abc_classification_sale_stock -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 10.0+e\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-02-15 16:51+0000\n" -"PO-Revision-Date: 2021-02-15 16:51+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_sale_stock -#: model:ir.model,name:product_abc_classification_sale_stock.model_abc_classification_product_level -msgid "Abc Classification Product Level" -msgstr "Class / Niveau ABC" - -#. module: product_abc_classification_sale_stock -#: model:ir.model,name:product_abc_classification_sale_stock.model_abc_classification_profile -msgid "Abc Classification Profile" -msgstr "Profil de classification ABC" - -#. module: product_abc_classification_sale_stock -#: model:ir.actions.act_window,name:product_abc_classification_sale_stock.abc_sale_stock_level_history_act_window -#: model:ir.model,name:product_abc_classification_sale_stock.model_abc_sale_stock_level_history -#: model:ir.ui.menu,name:product_abc_classification_sale_stock.abc_sale_stock_level_history_menu -msgid "Abc Sale_stock Level History" -msgstr "ABC: Historique des données de calul (basé sur les ventes)" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_computed_level_id -msgid "Computed classification level" -msgstr "Classe calculée" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_create_uid -msgid "Created by" -msgstr "Créé par" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_create_date -msgid "Created on" -msgstr "Créé le" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_cumulated_percentage -msgid "Cumulated percentage" -msgstr "Pourcentage cumulé" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,help:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_cumulated_percentage -msgid "Cumulated percentage of all the products with a better ranking" -msgstr "Pourcentage cumulé de tous les articles avec un meilleur classement" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_cumulated_percentage_products -msgid "Cumulated percentage of products" -msgstr "Pourcentage cumulé des articles" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,help:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_cumulated_percentage_products -msgid "Cumulated percentage of total products analyzed with a better ranking" -msgstr "Pourcentage de tous les article avec un meilleur classement" - -#. module: product_abc_classification_sale_stock -#: code:addons/product_abc_classification_sale_stock/models/abc_classification_profile.py:285 -#, python-format -msgid "Cumulative percentage greater than 100." -msgstr "Somme des pourcentages calculés supérieure à 100." - -#. module: product_abc_classification_sale_stock -#: model:ir.ui.view,arch_db:product_abc_classification_sale_stock.abc_sale_stock_level_history_search_view -msgid "Data collected this year" -msgstr "Données collectées cette année" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_display_name -msgid "Display Name" -msgstr "Nom affiché" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_from_date -msgid "From date" -msgstr "De" - -#. module: product_abc_classification_sale_stock -#: model:ir.ui.view,arch_db:product_abc_classification_sale_stock.abc_sale_stock_level_history_search_view -msgid "Group By" -msgstr "Grouper par" - -#. module: product_abc_classification_sale_stock -#: model:ir.ui.view,arch_db:product_abc_classification_sale_stock.abc_classification_product_level_form_view -msgid "History" -msgstr "Historique" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_id -msgid "ID" -msgstr "ID" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history___last_update -msgid "Last Modified on" -msgstr "Dernière modification le" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_write_uid -msgid "Last Updated by" -msgstr "Dernière mise à jour par" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_write_date -msgid "Last Updated on" -msgstr "Dernière mise à jour le" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_number_so_lines -msgid "Number of sale order lines" -msgstr "Nombre de ligne de vente" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_percentage -msgid "Percentage" -msgstr "Pourcentage" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_percentage_products -msgid "Percentage of products" -msgstr "Pourcentage des articles" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,help:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_percentage_products -msgid "Percentage of total products analyzed" -msgstr "Percentage of total products analyzed" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,help:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_percentage -msgid "Percentage of total sale order lines" -msgstr "Pourcentage du nombre total de toutes les lignes de ventes" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_product_id -msgid "Product" -msgstr "Article" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_product_level_id -msgid "Product level id" -msgstr "Classement ABC de l'article" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_product_tmpl_id -msgid "Product template" -msgstr "Modèle de produit" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_profile_id -#: model:ir.ui.view,arch_db:product_abc_classification_sale_stock.abc_sale_stock_level_history_search_view -msgid "Profile" -msgstr "Profil" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_ranking -msgid "Ranking" -msgstr "Classement" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,help:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_ranking -msgid "Ranking by number of oder lines" -msgstr "Classement par nombre de lignes de vente" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_classification_product_level_sale_stock_level_history_ids -msgid "Sale stock level history ids" -msgstr "Historique des données de vente collectées" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,help:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_sum_cumulated_percentages -msgid "Sum cumulated % so lines and cumulated % products" -msgstr "Somme %s cumulé lignes de vente et % cumulé articles" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_sum_cumulated_percentages -msgid "Sum of cummulated percentages" -msgstr "Somme % cumulés" - -#. module: product_abc_classification_sale_stock -#: model:ir.ui.view,arch_db:product_abc_classification_sale_stock.abc_sale_stock_level_history_search_view -msgid "This Year" -msgstr "Cette année" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_to_date -msgid "To date" -msgstr "A" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_total_so_lines -msgid "Total of sale order lines" -msgstr "Total des lignes de vente" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_total_products -msgid "Total products analysed" -msgstr "Total articles analysés" - -#. module: product_abc_classification_sale_stock -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_classification_profile_warehouse_id -#: model:ir.model.fields,field_description:product_abc_classification_sale_stock.field_abc_sale_stock_level_history_warehouse_id -msgid "Warehouse" -msgstr "Entrepôt" - -#. module: product_abc_classification_sale_stock -#: code:addons/product_abc_classification_sale_stock/models/abc_classification_profile.py:41 -#, python-format -msgid "You must specify a warehouse for {profile_name}" -msgstr "Vous devez specifier un entrepôt pour le profil {profile_name}" diff --git a/product_abc_classification_sale_stock/models/__init__.py b/product_abc_classification_sale_stock/models/__init__.py deleted file mode 100644 index d64de14ddd6c..000000000000 --- a/product_abc_classification_sale_stock/models/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from . import abc_classification_profile -from . import abc_classification_product_level -from . import abc_sale_stock_level_history diff --git a/product_abc_classification_sale_stock/models/abc_classification_product_level.py b/product_abc_classification_sale_stock/models/abc_classification_product_level.py deleted file mode 100644 index c59b9740ead3..000000000000 --- a/product_abc_classification_sale_stock/models/abc_classification_product_level.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2021 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo import fields, models - - -class AbcClassificationProductLevel(models.Model): - - _inherit = "abc.classification.product.level" - - sale_stock_level_history_ids = fields.One2many( - comodel_name="abc.sale_stock.level.history", - inverse_name="product_level_id", - ) diff --git a/product_abc_classification_sale_stock/models/abc_classification_profile.py b/product_abc_classification_sale_stock/models/abc_classification_profile.py deleted file mode 100644 index c38641fc6e47..000000000000 --- a/product_abc_classification_sale_stock/models/abc_classification_profile.py +++ /dev/null @@ -1,425 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2021 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -import csv -from cStringIO import StringIO -from datetime import datetime, timedelta -from operator import attrgetter - -from odoo import _, api, fields, models -from odoo.tools import float_round -from odoo.exceptions import UserError, ValidationError - - -class AbcClassificationProfile(models.Model): - - _inherit = "abc.classification.profile" - - profile_type = fields.Selection( - selection_add=[ - ( - "sale_stock", - "Based on the count of delivered sale order line by product", - ) - ] - ) - warehouse_id = fields.Many2one( - "stock.warehouse", - "Warehouse", - ondelete="cascade", - default=lambda self: self.env["stock.warehouse"].search( - [("company_id", "=", self.env.user.company_id.id)], limit=1 - ), - ) - - @api.constrains("profile_type", "warehouse_id") - def _check_warehouse_id(self): - for rec in self: - if rec.profile_type == "sale_stock" and not rec.warehouse_id: - raise ValidationError( - _( - "You must specify a warehouse for {profile_name}" - ).forman(profile_name=rec.name) - ) - - @api.model - def _get_collected_data_class(self): - return SaleStockData - - def _init_collected_data_instance(self): - self.ensure_one() - sale_stock_data = self._get_collected_data_class()() - sale_stock_data.profile = self - return sale_stock_data - - def _get_all_product_ids(self): - """Get a set of product ids with the current profile""" - self.ensure_one() - self.env.cr.execute( - """ - SELECT - product_id - FROM - abc_classification_profile_product_rel - JOIN - product_product pp - ON pp.id = product_id - WHERE - pp.active - AND profile_id = %(profile_id)s - """, - {"profile_id": self.id}, - ) - return {r[0] for r in self.env.cr.fetchall()} - - def _get_data(self, from_date=None): - """Get a list of statics info from the DB ordered by number of lines desc - """ - self.ensure_one() - from_date = ( - from_date - if from_date - else fields.Datetime.to_string( - datetime.today() - timedelta(days=self.period) - ) - ) - to_date = datetime.today() - customer_location_ids = ( - self.env["stock.location"].search([("usage", "=", "customer")]).ids - ) - # Collect all the product linked to the profile to be sure to provide - # information also for product no sold into the given period - all_product_ids = self._get_all_product_ids() - - # Count the number of delivered order line by product linked to a - # stock_move with a customer location as destination and a date later - # than the given date - self.env.cr.execute( - """ SELECT - sol.product_id product_id, - COUNT(sol.id) number_so_lines - FROM - sale_order so - JOIN - sale_order_line sol ON - sol.order_id = so.id - JOIN - abc_classification_profile_product_rel rel - ON rel.product_id = sol.product_id - JOIN - product_product pp - ON pp.id = sol.product_id - WHERE sol.qty_delivered > 0 - AND pp.active - AND rel.profile_id = %(profile_id)s - AND so.warehouse_id = %(current_warehouse_id)s - AND EXISTS ( - SELECT - 1 - FROM - stock_move sm - JOIN - procurement_order po - on po.id = sm.procurement_id - WHERE - sm.date > %(start_date)s - AND sm.location_dest_id in %(customer_loc_ids)s - AND po.sale_line_id = sol.id - ) - - GROUP BY sol.product_id - ORDER BY number_so_lines DESC - """, - { - "start_date": from_date, - "current_warehouse_id": self.warehouse_id.id, - "profile_id": self.id, - "customer_loc_ids": tuple(customer_location_ids), - }, - ) - - result = self.env.cr.fetchall() - - total = 0 - sale_stock_data_list = [] - ranking = 1 - ProductProduct = self.env["product.product"] - for r in result: - sale_stock_data = self._init_collected_data_instance() - product_id = r[0] - sale_stock_data.product = ProductProduct.browse(product_id) - sale_stock_data.number_so_lines = int(r[1]) - sale_stock_data.ranking = ranking - sale_stock_data.from_date = from_date - sale_stock_data.to_date = to_date - ranking += 1 - total += int(r[1]) - sale_stock_data_list.append(sale_stock_data) - all_product_ids.remove(product_id) - - # Add all products not sold or not delivered into this timelapse - for product_id in all_product_ids: - sale_stock_data = self._init_collected_data_instance() - sale_stock_data.product = ProductProduct.browse(product_id) - sale_stock_data.number_so_lines = 0 - sale_stock_data.ranking = ranking - sale_stock_data.from_date = from_date - sale_stock_data.to_date = to_date - sale_stock_data_list.append(sale_stock_data) - - return sale_stock_data_list, total - - def _build_ordered_level_cumulative_percentage(self): - """Return an ordered list of tuple of level, cumulative percentage - - The ordering is based on the level with the higher percentage first - """ - self.ensure_one() - levels = self.level_ids.sorted( - key=attrgetter("percentage"), reverse=True - ) - cum_percentages = [] - previous_percentage = None - for i, level in enumerate(levels): - perc = level.percentage + level.percentage_products - if i == 0: - percentage_to_append = perc - cum_percentages.append(percentage_to_append) - else: - percentage_to_append = previous_percentage + perc - cum_percentages.append(percentage_to_append) - previous_percentage = percentage_to_append - - return list(zip(levels, cum_percentages)) - - def _get_existing_level_ids(self): - self.ensure_one() - self.env.cr.execute( - """ - SELECT - id - FROM - abc_classification_product_level - WHERE - profile_id = %(profile_id)s - """, - {"profile_id": self.id}, - ) - return {r[0] for r in self.env.cr.fetchall()} - - def _purge_obsolete_level_values(self, ids_to_remove): - if not ids_to_remove: - return - self.env.cr.execute( - """ - DELETE FROM - abc_classification_product_level - WHERE - id in %(ids)s - """, - {"ids": tuple(ids_to_remove)}, - ) - - def _sale_stock_data_to_vals(self, sale_stock_data, create=False): - self.ensure_one() - res = {"computed_level_id": sale_stock_data.computed_level.id} - if create: - res.update( - { - "product_id": sale_stock_data.product.id, - "profile_id": sale_stock_data.profile.id, - } - ) - return res - - @api.multi - def _compute_abc_classification(self): - to_compute = self.filtered((lambda p: p.profile_type == "sale_stock")) - remaining = self - to_compute - res = None - if remaining: - res = super( - AbcClassificationProfile, remaining - )._compute_abc_classification() - ProductClassification = self.env["abc.classification.product.level"] - - for profile in to_compute: - sale_stock_data_list, total = profile._get_data() - existing_level_ids_to_remove = profile._get_existing_level_ids() - level_percentage = ( - profile._build_ordered_level_cumulative_percentage() - ) - level, percentage = level_percentage.pop(0) - previous_data = {} - total_products = len(sale_stock_data_list) - percentage_products = 100.0 / total_products - for i, sale_stock_data in enumerate(sale_stock_data_list): - sale_stock_data.total_products = total_products - sale_stock_data.percentage_products = percentage_products - sale_stock_data.cumulated_percentage_products = ( - sale_stock_data.percentage_products - if i == 0 - else ( - sale_stock_data.percentage_products - + previous_data.cumulated_percentage_products - ) - ) - # Compute percentages and cumulative percentages for the products - sale_stock_data.percentage = ( - (100.0 * sale_stock_data.number_so_lines / total) - if total - else 0.0 - ) - - sale_stock_data.cumulated_percentage = ( - sale_stock_data.percentage - if i == 0 - else ( - sale_stock_data.percentage - + previous_data.cumulated_percentage - ) - ) - if float_round(sale_stock_data.cumulated_percentage, 0) > 100: - raise UserError( - _("Cumulative percentage greater than 100.") - ) - - sale_stock_data.sum_cumulated_percentages = ( - sale_stock_data.cumulated_percentage - + sale_stock_data.cumulated_percentage_products - ) - - # Compute ABC classification for the products based on the - # sum of cumulated percentages - - if ( - sale_stock_data.sum_cumulated_percentages > percentage - and len(level_percentage) > 0 - ): - level, percentage = level_percentage.pop(0) - - product = sale_stock_data.product - levels = product.abc_classification_product_level_ids - product_abc_classification = levels.filtered( - lambda p, prof=profile: p.profile_id == prof - ) - - sale_stock_data.computed_level = level - if product_abc_classification: - # The line is still significant... - existing_level_ids_to_remove.remove( - product_abc_classification.id - ) - if product_abc_classification.level_id != level: - vals = profile._sale_stock_data_to_vals( - sale_stock_data, create=False - ) - product_abc_classification.write(vals) - else: - vals = profile._sale_stock_data_to_vals( - sale_stock_data, create=True - ) - product_abc_classification = ProductClassification.create( - vals - ) - sale_stock_data.total_so_lines = total - sale_stock_data.product_level = product_abc_classification - previous_data = sale_stock_data - self._log_history(sale_stock_data_list) - profile._purge_obsolete_level_values(existing_level_ids_to_remove) - return res - - def _log_history(self, sale_stock_data_list): - """ Log collected and computed values into - abc.sale_stock.level.history - - """ - vals = StringIO() - writer = csv.writer(vals, delimiter=";") - for sale_stock_data in sale_stock_data_list: - writer.writerow(sale_stock_data._to_csv_line()) - vals.seek(0) - table = self.env["abc.sale_stock.level.history"]._table - columns = sale_stock_data._get_col_names() - self.env.cr.copy_from(vals, table, columns=columns, sep=";") - self.env["abc.classification.product.level"].invalidate_cache( - ["sale_stock_level_history_ids"] - ) - - -class SaleStockData(object): - """ Sale stock collected data - - This class is used to store all the data collectd and computed for - a abc classification product level. It also provide methods used to bulk - insert these data into the abc.sale_stock.level.history table. - - """ - - __slots__ = [ - "product", - "profile", - "computed_level", - "ranking", - "percentage", - "cumulated_percentage", - "number_so_lines", - "total_so_lines", - "product_level", - "from_date", - "to_date", - "total_products", - "percentage_products", - "cumulated_percentage_products", - "sum_cumulated_percentages", - ] - - def _to_csv_line(self): - """Return values to write into a csv file""" - return [ - self.product.id, - self.product.product_tmpl_id.id, - self.profile.id, - self.computed_level.id, - self.profile.warehouse_id.id, - self.ranking, - self.percentage, - self.cumulated_percentage, - self.number_so_lines, - self.total_so_lines, - self.product_level.id, - self.from_date, - self.to_date, - self.total_products, - self.percentage_products, - self.cumulated_percentage_products, - self.sum_cumulated_percentages, - ] - - @classmethod - def _get_col_names(cls): - """Return the ordered list of column names related to the values - returned by _to_csv_line - - We use the name of the columns defined into abc.sale_stock.level.history - """ - return [ - "product_id", - "product_tmpl_id", - "profile_id", - "computed_level_id", - "warehouse_id", - "ranking", - "percentage", - "cumulated_percentage", - "number_so_lines", - "total_so_lines", - "product_level_id", - "from_date", - "to_date", - "total_products", - "percentage_products", - "cumulated_percentage_products", - "sum_cumulated_percentages", - ] diff --git a/product_abc_classification_sale_stock/models/abc_sale_stock_level_history.py b/product_abc_classification_sale_stock/models/abc_sale_stock_level_history.py deleted file mode 100644 index 301e089a643d..000000000000 --- a/product_abc_classification_sale_stock/models/abc_sale_stock_level_history.py +++ /dev/null @@ -1,125 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2021 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo import fields, models - - -class AbcSaleStockLevelHistory(models.Model): - """ ABC Classification Product Level History - - This model is used to display the history of values collected and involved - into the computation of the ABC classification level. - - To avoid performance issue, the table is populated by bypassing the ORM - since a new line is inserted by product and classification profile, - each time the computation of the classification levels occurs. - - Some could argue that the same functionality could be achieved by using the - tracking of changes mechanism provided by mail.thread. Nevertheless, - mail.thread introduce a to high performance footprint and the result is not - usable into reports - """ - - _name = "abc.sale_stock.level.history" - _description = "Abc Sale_stock Level History" - - computed_level_id = fields.Many2one( - "abc.classification.level", - string="Computed classification level", - readonly=True, - ondelete="cascade", - ) - product_id = fields.Many2one( - "product.product", - string="Product", - index=True, - required=True, - readonly=True, - ondelete="cascade", - ) - product_tmpl_id = fields.Many2one( - "product.template", - string="Product template", - related="product_id.product_tmpl_id", - readonly=True, - store=True, - ) - # percentage - profile_id = fields.Many2one( - "abc.classification.profile", - string="Profile", - required=True, - readonly=True, - ondelete="cascade", - ) - product_level_id = fields.Many2one( - "abc.classification.product.level", - required=True, - index=True, - readonly=True, - ondelete="cascade", - ) - warehouse_id = fields.Many2one( - "stock.warehouse", "Warehouse", readonly=False, ondelete="cascade", - ) - ranking = fields.Integer( - "Ranking", - required=True, - readonly=True, - help="Ranking by number of oder lines", - ) - number_so_lines = fields.Integer( - "Number of sale order lines", required=True, readonly=True, - ) - total_so_lines = fields.Integer( - "Total of sale order lines", required=True, readonly=True, - ) - percentage = fields.Float( - "Percentage", - required=True, - readonly=True, - help="Percentage of total sale order lines", - digits=(7, 4), - group_operator="SUM", - ) - cumulated_percentage = fields.Float( - "Cumulated percentage", - required=True, - readonly=True, - help="Cumulated percentage of all the products with a better ranking", - digits=(7, 4), - group_operator=None, - ) - total_products = fields.Integer( - "Total products analysed", - required=True, - readonly=True, - group_operator=None, - ) - percentage_products = fields.Float( - "Percentage of products", - required=True, - readonly=True, - help="Percentage of total products analyzed", - digits=(7, 4), - group_operator="SUM", - ) - cumulated_percentage_products = fields.Float( - "Cumulated percentage of products", - required=True, - readonly=True, - help="Cumulated percentage of total products analyzed with a " - "better ranking", - digits=(7, 4), - group_operator=None, - ) - sum_cumulated_percentages = fields.Float( - "Sum of cummulated percentages", - required=True, - readonly=True, - help="Sum cumulated % so lines and cumulated % products", - group_operator=None, - ) - from_date = fields.Date(readonly=True) - to_date = fields.Date(readonly=True) diff --git a/product_abc_classification_sale_stock/readme/CONTRIBUTORS.rst b/product_abc_classification_sale_stock/readme/CONTRIBUTORS.rst deleted file mode 100644 index 2223963b2124..000000000000 --- a/product_abc_classification_sale_stock/readme/CONTRIBUTORS.rst +++ /dev/null @@ -1,2 +0,0 @@ -* Lindsay Marion -* Laurent Mignon diff --git a/product_abc_classification_sale_stock/readme/DESCRIPTION.rst b/product_abc_classification_sale_stock/readme/DESCRIPTION.rst deleted file mode 100644 index d289b47a2423..000000000000 --- a/product_abc_classification_sale_stock/readme/DESCRIPTION.rst +++ /dev/null @@ -1,2 +0,0 @@ -This modules includes an ABC analysis computation profile based -on the number of sale order lines delivered from a given date by product. diff --git a/product_abc_classification_sale_stock/readme/USAGE.rst b/product_abc_classification_sale_stock/readme/USAGE.rst deleted file mode 100644 index 188a95ba3c76..000000000000 --- a/product_abc_classification_sale_stock/readme/USAGE.rst +++ /dev/null @@ -1,8 +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 variants, and assign them a profile. -Then the cron classification will proceed to assign to these products one of the profile's levels. diff --git a/product_abc_classification_sale_stock/security/abc_sale_stock_level_history.xml b/product_abc_classification_sale_stock/security/abc_sale_stock_level_history.xml deleted file mode 100644 index 0b2310cf480f..000000000000 --- a/product_abc_classification_sale_stock/security/abc_sale_stock_level_history.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - abc.sale_stock.level.history access name - - - - - - - - - - diff --git a/product_abc_classification_sale_stock/static/description/icon.png b/product_abc_classification_sale_stock/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_sale_stock/static/description/index.html b/product_abc_classification_sale_stock/static/description/index.html deleted file mode 100644 index 2821f7cb9798..000000000000 --- a/product_abc_classification_sale_stock/static/description/index.html +++ /dev/null @@ -1,431 +0,0 @@ - - - - - - -Product Abc Classification based on delivered products - - - -
-

Product Abc Classification based on delivered products

- - -

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

-

This modules includes an ABC analysis computation profile based -on the number of sale order lines delivered from a given date 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 variants, and assign them a profile. -Then the cron classification will proceed to assign to these products one of the profile’s levels.

-
-
-

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
  • -
-
-
-

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_sale_stock/tests/__init__.py b/product_abc_classification_sale_stock/tests/__init__.py deleted file mode 100644 index bc83b0a6056a..000000000000 --- a/product_abc_classification_sale_stock/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import test_abc_classification_profile diff --git a/product_abc_classification_sale_stock/tests/test_abc_classification_profile.py b/product_abc_classification_sale_stock/tests/test_abc_classification_profile.py deleted file mode 100644 index 6976a0345999..000000000000 --- a/product_abc_classification_sale_stock/tests/test_abc_classification_profile.py +++ /dev/null @@ -1,278 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2021 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from freezegun import freeze_time - -from odoo.tests.common import SavepointCase - - -class TestABCClassificationProfile(SavepointCase): - @classmethod - def setUpClass(cls): - super(TestABCClassificationProfile, cls).setUpClass() - - cls.partner = cls.env["res.partner"].create( - {"name": "Unittest partner", "ref": "12344566777878"} - ) - - cls.warehouse = cls.env.ref("stock.warehouse0") - cls.warehouse.write( - { - "name": "Test Warehouse", - "reception_steps": "one_step", - "delivery_steps": "ship_only", - "code": "TST", - } - ) - - cls.stock_profile = cls.env.ref( - "product_abc_classification_sale_stock." - "abc_classification_profile_sale_stock" - ) - cls.level_A = cls.env.ref( - "product_abc_classification_sale_stock.abc_classification_level_a" - ) - cls.level_B = cls.env.ref( - "product_abc_classification_sale_stock.abc_classification_level_b" - ) - cls.level_C = cls.env.ref( - "product_abc_classification_sale_stock.abc_classification_level_c" - ) - - cls.product1 = cls.env["product.product"].create( - { - "name": "Product1", - "uom_id": cls.env.ref("product.product_uom_unit").id, - "type": "product", - "default_code": "987654321", - "tracking": "none", - "abc_classification_profile_ids": [(4, cls.stock_profile.id)], - } - ) - - cls.product2 = cls.env["product.product"].create( - { - "name": "Product2", - "uom_id": cls.env.ref("product.product_uom_unit").id, - "type": "product", - "default_code": "123456789", - "tracking": "none", - "abc_classification_profile_ids": [(4, cls.stock_profile.id)], - } - ) - - cls.product3 = cls.env["product.product"].create( - { - "name": "Product3", - "uom_id": cls.env.ref("product.product_uom_unit").id, - "type": "product", - "default_code": "67548309", - "tracking": "none", - "abc_classification_profile_ids": [(4, cls.stock_profile.id)], - } - ) - - cls.product4 = cls.env["product.product"].create( - { - "name": "Product4", - "uom_id": cls.env.ref("product.product_uom_unit").id, - "type": "product", - "default_code": "123409876", - "tracking": "none", - "abc_classification_profile_ids": [(4, cls.stock_profile.id)], - } - ) - - cls.product5 = cls.env["product.product"].create( - { - "name": "Product5", - "uom_id": cls.env.ref("product.product_uom_unit").id, - "type": "product", - "default_code": "0987540321", - "tracking": "none", - "abc_classification_profile_ids": [(4, cls.stock_profile.id)], - } - ) - - cls.product6 = cls.env["product.product"].create( - { - "name": "Product6", - "uom_id": cls.env.ref("product.product_uom_unit").id, - "type": "product", - "default_code": "345789732", - "tracking": "none", - "abc_classification_profile_ids": [(4, cls.stock_profile.id)], - } - ) - # Special case where the product is not yet sold nor delivered - cls.product_new = cls.env["product.product"].create( - { - "name": "product_new", - "uom_id": cls.env.ref("product.product_uom_unit").id, - "type": "product", - "default_code": "345789733", - "tracking": "none", - "abc_classification_profile_ids": [(4, cls.stock_profile.id)], - } - ) - - cls._create_availability(cls.product1) - cls._create_availability(cls.product2) - cls._create_availability(cls.product3) - cls._create_availability(cls.product4) - cls._create_availability(cls.product5) - cls._create_availability(cls.product6) - - cls.so1 = cls._confirm_sale_order( - products=[cls.product1, cls.product2, cls.product3], - qty={ - cls.product1.name: 80, - cls.product2.name: 10, - cls.product3.name: 30, - }, - ) - cls._confirm_ship(cls.so1) - - cls.so2 = cls._confirm_sale_order( - products=[cls.product4, cls.product5, cls.product6], - qty={ - cls.product4.name: 5, - cls.product5.name: 30, - cls.product6.name: 25, - }, - ) - cls._confirm_ship(cls.so2) - - cls.so3 = cls._confirm_sale_order( - products=[cls.product1], qty={cls.product1.name: 75} - ) - cls._confirm_ship(cls.so3) - - cls.so3 = cls._confirm_sale_order( - products=[cls.product1], qty={cls.product1.name: 75} - ) - cls._confirm_ship(cls.so3) - - cls.so4 = cls._confirm_sale_order( - products=[cls.product1], qty={cls.product1.name: 25} - ) - cls._confirm_ship(cls.so4) - - cls.so5 = cls._confirm_sale_order( - products=[cls.product3, cls.product5], - qty={cls.product3.name: 90, cls.product5.name: 50}, - ) - cls._confirm_ship(cls.so5) - - cls.so6 = cls._confirm_sale_order( - products=[cls.product6], qty={cls.product6.name: 30} - ) - cls._confirm_ship(cls.so6) - - @classmethod - def _create_availability(cls, product): - update_qty_wizard = cls.env["stock.change.product.qty"].create( - { - "product_id": product.id, - "product_tmpl_id": product.product_tmpl_id.id, - "new_quantity": 500, - "location_id": cls.warehouse.lot_stock_id.id, - } - ) - update_qty_wizard.change_product_qty() - - @classmethod - def _confirm_sale_order(cls, products, qty, partner=None): - if partner is None: - partner = cls.partner - warehouse = cls.warehouse - Sale = cls.env["sale.order"] - lines = [ - ( - 0, - 0, - { - "name": p.name, - "product_id": p.id, - "product_uom_qty": qty[p.name], - "product_uom": p.uom_id.id, - "price_unit": 1, - }, - ) - for p in products - ] - so_values = { - "partner_id": partner.id, - "warehouse_id": warehouse.id, - "order_line": lines, - } - so = Sale.create(so_values) - so.action_confirm() - return so - - @classmethod - def _confirm_ship(cls, so): - pick = so.mapped("picking_ids") - pick.action_confirm() - pick.action_assign() - for pack_op in pick.pack_operation_ids: - pack_op.qty_done = pack_op.product_qty - pick.action_done() - - def _assertLevelIs(self, product, level_name): - levels = product.abc_classification_product_level_ids - self.assertEqual( - levels.computed_level_id.name, - level_name, - "{} should be classified as {}".format(product.name, level_name), - ) - levels = product.product_tmpl_id.abc_classification_product_level_ids - self.assertEqual( - levels.computed_level_id.name, - level_name, - "{} template should be classified as {}".format(product.name, level_name), - ) - - @freeze_time("2021-01-01 07:10:00") - def test_00(self): - # test computed classification and check that the classification is - # also set on the product_templale - self.stock_profile._compute_abc_classification() - self._assertLevelIs(self.product1, "a") - self._assertLevelIs(self.product3, "a") - self._assertLevelIs(self.product5, "b") - self._assertLevelIs(self.product6, "b") - self._assertLevelIs(self.product2, "c") - self._assertLevelIs(self.product4, "c") - self._assertLevelIs(self.product_new, "c") - - @freeze_time("2021-01-01 07:10:00") - def test_01(self): - # test computed classification and check that inactive products are - # not taken into account - self.product1.active = False - self.product1.refresh() - self.stock_profile._compute_abc_classification() - self.assertFalse(self.product1.abc_classification_product_level_ids) - self.product1.active = True - self.product1.refresh() - self.stock_profile._compute_abc_classification() - self.assertTrue(self.product1.abc_classification_product_level_ids) - self.product1.active = False - self.product1.refresh() - self.stock_profile._compute_abc_classification() - self.assertFalse(self.product1.abc_classification_product_level_ids) - - @freeze_time("2021-01-01 07:10:00") - def test_02(self): - # check that a line is created into the history value for each - # computed classification level each time a compute is done - levels = self.product1.abc_classification_product_level_ids - self.assertFalse(levels.sale_stock_level_history_ids) - self.stock_profile._compute_abc_classification() - levels = self.product1.abc_classification_product_level_ids - self.assertEqual(len(levels.sale_stock_level_history_ids), 1) - self.stock_profile._compute_abc_classification() - self.assertEqual(len(levels.sale_stock_level_history_ids), 2) - diff --git a/product_abc_classification_sale_stock/views/abc_classification_product_level.xml b/product_abc_classification_sale_stock/views/abc_classification_product_level.xml deleted file mode 100644 index 4c42652174a9..000000000000 --- a/product_abc_classification_sale_stock/views/abc_classification_product_level.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - abc.classification.product.level.form (in product_abc_classification_sale_stock) - abc.classification.product.level - - - - - - - - - - - - - - - - - - - - - diff --git a/product_abc_classification_sale_stock/views/abc_classification_profile.xml b/product_abc_classification_sale_stock/views/abc_classification_profile.xml deleted file mode 100644 index ccaa0f9f654d..000000000000 --- a/product_abc_classification_sale_stock/views/abc_classification_profile.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - abc.classification.profile.form (in product_abc_classification_sale_stock) - abc.classification.profile - - - - - - - - diff --git a/product_abc_classification_sale_stock/views/abc_sale_stock_level_history.xml b/product_abc_classification_sale_stock/views/abc_sale_stock_level_history.xml deleted file mode 100644 index 27b8e73e1d77..000000000000 --- a/product_abc_classification_sale_stock/views/abc_sale_stock_level_history.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - abc.sale_stock.level.history.search (in product_abc_classification_sale_stock) - abc.sale_stock.level.history - - - - - - - - - - - - - - - abc.sale_stock.level.history.tree (in product_abc_classification_sale_stock) - abc.sale_stock.level.history - - - - - - - - - - - - - - - - - - - - Abc Sale_stock Level History - abc.sale_stock.level.history - tree,form - [] - {'search_default_thisyear': 1} - - - - Abc Sale_stock Level History - - - - - - - diff --git a/setup/product_abc_classification_sale_stock/.eggs/README.txt b/setup/product_abc_classification_sale_stock/.eggs/README.txt deleted file mode 100644 index 5d01668824f4..000000000000 --- a/setup/product_abc_classification_sale_stock/.eggs/README.txt +++ /dev/null @@ -1,6 +0,0 @@ -This directory contains eggs that were downloaded by setuptools to build, test, and run plug-ins. - -This directory caches those eggs to prevent repeated downloads. - -However, it is safe to delete this directory. - diff --git a/setup/product_abc_classification_sale_stock/odoo/__init__.py b/setup/product_abc_classification_sale_stock/odoo/__init__.py deleted file mode 100644 index de40ea7ca058..000000000000 --- a/setup/product_abc_classification_sale_stock/odoo/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__import__('pkg_resources').declare_namespace(__name__) diff --git a/setup/product_abc_classification_sale_stock/odoo/addons/__init__.py b/setup/product_abc_classification_sale_stock/odoo/addons/__init__.py deleted file mode 100644 index de40ea7ca058..000000000000 --- a/setup/product_abc_classification_sale_stock/odoo/addons/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__import__('pkg_resources').declare_namespace(__name__) diff --git a/setup/product_abc_classification_sale_stock/odoo/addons/product_abc_classification_sale_stock b/setup/product_abc_classification_sale_stock/odoo/addons/product_abc_classification_sale_stock deleted file mode 120000 index b9bb5396d797..000000000000 --- a/setup/product_abc_classification_sale_stock/odoo/addons/product_abc_classification_sale_stock +++ /dev/null @@ -1 +0,0 @@ -../../../../product_abc_classification_sale_stock \ No newline at end of file diff --git a/setup/product_abc_classification_sale_stock/setup.py b/setup/product_abc_classification_sale_stock/setup.py deleted file mode 100644 index 28c57bb64031..000000000000 --- a/setup/product_abc_classification_sale_stock/setup.py +++ /dev/null @@ -1,6 +0,0 @@ -import setuptools - -setuptools.setup( - setup_requires=['setuptools-odoo'], - odoo_addon=True, -) From 6de35e517ca230c45976ebeeb15b74d43e4d587e Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Thu, 10 Nov 2022 11:57:34 +0100 Subject: [PATCH 12/21] [MIG][16.0] product_abc_classification_base --- product_abc_classification_base/README.rst | 17 ++++++----- .../__manifest__.py | 4 +-- .../data/ir_cron.xml | 6 ++-- .../models/abc_classification_level.py | 1 + .../abc_classification_product_level.py | 29 ++++++++++--------- .../models/abc_classification_profile.py | 3 +- .../readme/CONTRIBUTORS.rst | 1 + .../static/description/index.html | 15 +++++----- .../tests/common.py | 15 +++++----- .../test_abc_classification_product_level.py | 2 +- .../tests/test_product.py | 2 +- .../abc_classification_product_level.xml | 2 +- .../views/abc_classification_profile.xml | 2 +- .../odoo/__init__.py | 1 - .../odoo/addons/__init__.py | 1 - 15 files changed, 53 insertions(+), 48 deletions(-) delete mode 100644 setup/product_abc_classification_base/odoo/__init__.py delete mode 100644 setup/product_abc_classification_base/odoo/addons/__init__.py diff --git a/product_abc_classification_base/README.rst b/product_abc_classification_base/README.rst index 196e2ed4364b..bbed589ebf53 100644 --- a/product_abc_classification_base/README.rst +++ b/product_abc_classification_base/README.rst @@ -1,6 +1,6 @@ -============================== -Alc Product Abc Classification -============================== +========================== +Product Abc Classification +========================== .. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! @@ -14,13 +14,13 @@ Alc 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/10.0/product_abc_classification_base + :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-10-0/product-attribute-10-0-product_abc_classification_base + :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/10.0 + :target: https://runbot.odoo-community.org/runbot/135/16.0 :alt: Try me on Runbot |badge1| |badge2| |badge3| |badge4| |badge5| @@ -63,7 +63,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 smashing it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -82,6 +82,7 @@ Contributors * Miquel Raïch * Lindsay Marion * Laurent Mignon +* Denis Roussel Maintainers ~~~~~~~~~~~ @@ -96,6 +97,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_base/__manifest__.py b/product_abc_classification_base/__manifest__.py index f0f46384aa18..cab1805d35e3 100644 --- a/product_abc_classification_base/__manifest__.py +++ b/product_abc_classification_base/__manifest__.py @@ -6,10 +6,11 @@ "name": "Product Abc Classification", "summary": """ ABC classification for sales and warehouse management""", - "version": "10.0.1.0.0", + "version": "16.0.1.0.0", "license": "AGPL-3", "author": "ACSONE SA/NV, ForgeFlow, Odoo Community Association (OCA)", "depends": ["product", "stock", "web_m2x_options"], + "website": "https://github.com/OCA/product-attribute", "data": [ "views/abc_classification_product_level.xml", "views/abc_classification_profile.xml", @@ -18,5 +19,4 @@ "security/ir.model.access.csv", "data/ir_cron.xml", ], - "demo": [], } diff --git a/product_abc_classification_base/data/ir_cron.xml b/product_abc_classification_base/data/ir_cron.xml index a8bb93fd5460..04303328c198 100644 --- a/product_abc_classification_base/data/ir_cron.xml +++ b/product_abc_classification_base/data/ir_cron.xml @@ -7,8 +7,8 @@ months -1 - abc.classification.profile - _cron_compute_abc_classification - () + + model._cron_compute_abc_classification() + code diff --git a/product_abc_classification_base/models/abc_classification_level.py b/product_abc_classification_base/models/abc_classification_level.py index 5144c5bb07ad..01b045eabc72 100644 --- a/product_abc_classification_base/models/abc_classification_level.py +++ b/product_abc_classification_base/models/abc_classification_level.py @@ -9,6 +9,7 @@ class AbcClassificationLevel(models.Model): _name = "abc.classification.level" + _description = "ABC Classification Level" _order = "percentage desc, id desc" _rec_name = "name" diff --git a/product_abc_classification_base/models/abc_classification_product_level.py b/product_abc_classification_base/models/abc_classification_product_level.py index 55690ec1bc69..1ad9cf6c1843 100644 --- a/product_abc_classification_base/models/abc_classification_product_level.py +++ b/product_abc_classification_base/models/abc_classification_product_level.py @@ -16,7 +16,7 @@ class AbcClassificationProductLevel(models.Model): manual_level_id = fields.Many2one( "abc.classification.level", string="Manual classification level", - track_visibility="onchange", + tracking=True, domain="[('profile_id', '=', profile_id)]", ) computed_level_id = fields.Many2one( @@ -129,18 +129,21 @@ def _compute_flag(self): rec.computed_level_id and rec.manual_level_id != rec.computed_level_id ) - @api.model - def create(self, vals): - 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: + @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"] - return super(AbcClassificationProductLevel, self).create(vals) + + 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() @@ -153,4 +156,4 @@ def write(self, vals): if profile.auto_apply_computed_value and "computed_level_id" in values: values["manual_level_id"] = values["computed_level_id"] - return super(AbcClassificationProductLevel, self).write(values) + return super().write(values) diff --git a/product_abc_classification_base/models/abc_classification_profile.py b/product_abc_classification_base/models/abc_classification_profile.py index 1c669d4a0ca1..b57b0a1e46d7 100644 --- a/product_abc_classification_base/models/abc_classification_profile.py +++ b/product_abc_classification_base/models/abc_classification_profile.py @@ -57,7 +57,6 @@ def _check_levels(self): ) ) - @api.multi def _compute_abc_classification(self): raise NotImplementedError() @@ -94,4 +93,4 @@ def _auto_apply_computed_value_for_product_levels(self): 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() + modified_levels._recompute_recordset() diff --git a/product_abc_classification_base/readme/CONTRIBUTORS.rst b/product_abc_classification_base/readme/CONTRIBUTORS.rst index ebb18f6ff748..fe41e2ce43dd 100644 --- a/product_abc_classification_base/readme/CONTRIBUTORS.rst +++ b/product_abc_classification_base/readme/CONTRIBUTORS.rst @@ -1,3 +1,4 @@ * Miquel Raïch * Lindsay Marion * Laurent Mignon +* Denis Roussel diff --git a/product_abc_classification_base/static/description/index.html b/product_abc_classification_base/static/description/index.html index dd0266cbce4e..b309d87953d4 100644 --- a/product_abc_classification_base/static/description/index.html +++ b/product_abc_classification_base/static/description/index.html @@ -3,8 +3,8 @@ - -Alc Product Abc Classification + +Product Abc Classification -
-

Alc Product Abc Classification

+
+

Product Abc Classification

-

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

+

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 @@ -406,7 +406,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 smashing it by providing a detailed and welcomed -feedback.

+feedback.

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

@@ -424,6 +424,7 @@

Contributors

  • Miquel Raïch <miquel.raich@eficent.com>
  • Lindsay Marion <lindsay.marion@acsone.eu>
  • Laurent Mignon <laurent.mignon@acsone.eu>
  • +
  • Denis Roussel <denis.roussel@acsone.eu>
  • @@ -433,7 +434,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_base/tests/common.py b/product_abc_classification_base/tests/common.py index f62e9dd2a41e..4900770fdce2 100644 --- a/product_abc_classification_base/tests/common.py +++ b/product_abc_classification_base/tests/common.py @@ -1,13 +1,14 @@ # Copyright 2021 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo.tests.common import SavepointCase +from odoo.tests.common import TransactionCase -class ABCClassificationCase(SavepointCase): +class ABCClassificationCase(TransactionCase): @classmethod def setUpClass(cls): - super(ABCClassificationCase, cls).setUpClass() + 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 = [ @@ -21,7 +22,7 @@ def setUpClass(cls): class ABCClassificationLevelCase(ABCClassificationCase): @classmethod def setUpClass(cls): - super(ABCClassificationLevelCase, cls).setUpClass() + super().setUpClass() cls.classification_profile.write( { "level_ids": [ @@ -85,13 +86,13 @@ def setUpClass(cls): cls.size_attr = cls.env["product.attribute"].create( { "name": "Size", - "create_variant": False, + "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("product.product_uom_unit") + cls.uom_unit = cls.env.ref("uom.product_uom_unit") cls.product_template = cls.env["product.template"].create( { "name": "Test sized", @@ -117,6 +118,6 @@ def _create_variant(cls, size_value): return cls.env["product.product"].create( { "product_tmpl_id": cls.product_template.id, - "attribute_value_ids": [(6, 0, size_value.ids)], + "product_template_attribute_value_ids": [(6, 0, size_value.ids)], } ) diff --git a/product_abc_classification_base/tests/test_abc_classification_product_level.py b/product_abc_classification_base/tests/test_abc_classification_product_level.py index fe87c93675ec..9b2db6df8722 100644 --- a/product_abc_classification_base/tests/test_abc_classification_product_level.py +++ b/product_abc_classification_base/tests/test_abc_classification_product_level.py @@ -11,7 +11,7 @@ class TestABCClassificationProductLevel(ABCClassificationLevelCase): @classmethod def setUpClass(cls): - super(TestABCClassificationProductLevel, cls).setUpClass() + super().setUpClass() cls.product_1 = cls.env["product.product"].create( { "name": "Test 1", diff --git a/product_abc_classification_base/tests/test_product.py b/product_abc_classification_base/tests/test_product.py index ed3406fcfaa4..923ae1eefe9b 100644 --- a/product_abc_classification_base/tests/test_product.py +++ b/product_abc_classification_base/tests/test_product.py @@ -7,7 +7,7 @@ class TestProduct(ABCClassificationLevelCase): @classmethod def setUpClass(cls): - super(TestProduct, cls).setUpClass() + super().setUpClass() def test_00(self): """ diff --git a/product_abc_classification_base/views/abc_classification_product_level.xml b/product_abc_classification_base/views/abc_classification_product_level.xml index fed469a26041..e6332542fe8f 100644 --- a/product_abc_classification_base/views/abc_classification_product_level.xml +++ b/product_abc_classification_base/views/abc_classification_product_level.xml @@ -20,7 +20,7 @@ diff --git a/product_abc_classification_base/views/abc_classification_profile.xml b/product_abc_classification_base/views/abc_classification_profile.xml index cca0af065376..781f0cd122c8 100644 --- a/product_abc_classification_base/views/abc_classification_profile.xml +++ b/product_abc_classification_base/views/abc_classification_profile.xml @@ -35,7 +35,7 @@ >abc.classification.profile.tree (in product_abc_classification_base) abc.classification.profile - + diff --git a/setup/product_abc_classification_base/odoo/__init__.py b/setup/product_abc_classification_base/odoo/__init__.py deleted file mode 100644 index de40ea7ca058..000000000000 --- a/setup/product_abc_classification_base/odoo/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__import__('pkg_resources').declare_namespace(__name__) diff --git a/setup/product_abc_classification_base/odoo/addons/__init__.py b/setup/product_abc_classification_base/odoo/addons/__init__.py deleted file mode 100644 index de40ea7ca058..000000000000 --- a/setup/product_abc_classification_base/odoo/addons/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__import__('pkg_resources').declare_namespace(__name__) From 26040ec06e67c3fcee456a39b5534cdabe2ac2b3 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Thu, 10 Nov 2022 12:05:07 +0100 Subject: [PATCH 13/21] [IMP] product_abc_classification_base: Remove web_m2x_options dependency --- product_abc_classification_base/__manifest__.py | 2 +- product_abc_classification_base/views/product_template.xml | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/product_abc_classification_base/__manifest__.py b/product_abc_classification_base/__manifest__.py index cab1805d35e3..310fe0ad7343 100644 --- a/product_abc_classification_base/__manifest__.py +++ b/product_abc_classification_base/__manifest__.py @@ -9,8 +9,8 @@ "version": "16.0.1.0.0", "license": "AGPL-3", "author": "ACSONE SA/NV, ForgeFlow, Odoo Community Association (OCA)", - "depends": ["product", "stock", "web_m2x_options"], "website": "https://github.com/OCA/product-attribute", + "depends": ["product", "stock"], "data": [ "views/abc_classification_product_level.xml", "views/abc_classification_profile.xml", diff --git a/product_abc_classification_base/views/product_template.xml b/product_abc_classification_base/views/product_template.xml index 3e49b0a2ab79..1127c274e5fb 100644 --- a/product_abc_classification_base/views/product_template.xml +++ b/product_abc_classification_base/views/product_template.xml @@ -19,15 +19,10 @@ name="abc_classification_product_level_ids" widget="many2many_tags" context="{'default_product_tmpl_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}" - options="{'open': true}" attrs="{'readonly': [('product_variant_count', '>', 1)]}" domain="[('product_tmpl_id', '=', active_id)]" /> - +
    From ae7e0a5b1a752606122c2dbb151ca38ec5322102 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Thu, 10 Nov 2022 14:21:24 +0100 Subject: [PATCH 14/21] [IMP] product_abc_classification/base: Restore module name --- .../README.rst | 0 .../__init__.py | 0 .../__manifest__.py | 0 .../data/ir_cron.xml | 0 .../i18n/fr.po | 266 +++++++++--------- .../models/__init__.py | 0 .../models/abc_classification_level.py | 0 .../abc_classification_product_level.py | 0 .../models/abc_classification_profile.py | 0 .../models/product_product.py | 0 .../models/product_template.py | 0 .../readme/CONTRIBUTORS.rst | 0 .../readme/DESCRIPTION.rst | 0 .../readme/USAGE.rst | 0 .../security/ir.model.access.csv | 0 .../static/description/icon.png | Bin .../static/description/index.html | 0 .../tests/__init__.py | 0 .../tests/common.py | 0 .../test_abc_classification_product_level.py | 0 .../tests/test_abc_classification_profile.py | 0 .../tests/test_product.py | 0 .../abc_classification_product_level.xml | 6 +- .../views/abc_classification_profile.xml | 4 +- .../views/product_product.xml | 0 .../views/product_template.xml | 0 .../odoo/addons/product_abc_classification | 1 + .../setup.py | 0 .../.eggs/README.txt | 6 - .../addons/product_abc_classification_base | 1 - 30 files changed, 139 insertions(+), 145 deletions(-) rename {product_abc_classification_base => product_abc_classification}/README.rst (100%) rename {product_abc_classification_base => product_abc_classification}/__init__.py (100%) rename {product_abc_classification_base => product_abc_classification}/__manifest__.py (100%) rename {product_abc_classification_base => product_abc_classification}/data/ir_cron.xml (100%) rename {product_abc_classification_base => product_abc_classification}/i18n/fr.po (50%) rename {product_abc_classification_base => product_abc_classification}/models/__init__.py (100%) rename {product_abc_classification_base => product_abc_classification}/models/abc_classification_level.py (100%) rename {product_abc_classification_base => product_abc_classification}/models/abc_classification_product_level.py (100%) rename {product_abc_classification_base => product_abc_classification}/models/abc_classification_profile.py (100%) rename {product_abc_classification_base => product_abc_classification}/models/product_product.py (100%) rename {product_abc_classification_base => product_abc_classification}/models/product_template.py (100%) rename {product_abc_classification_base => product_abc_classification}/readme/CONTRIBUTORS.rst (100%) rename {product_abc_classification_base => product_abc_classification}/readme/DESCRIPTION.rst (100%) rename {product_abc_classification_base => product_abc_classification}/readme/USAGE.rst (100%) rename {product_abc_classification_base => product_abc_classification}/security/ir.model.access.csv (100%) rename {product_abc_classification_base => product_abc_classification}/static/description/icon.png (100%) rename {product_abc_classification_base => product_abc_classification}/static/description/index.html (100%) rename {product_abc_classification_base => product_abc_classification}/tests/__init__.py (100%) rename {product_abc_classification_base => product_abc_classification}/tests/common.py (100%) rename {product_abc_classification_base => product_abc_classification}/tests/test_abc_classification_product_level.py (100%) rename {product_abc_classification_base => product_abc_classification}/tests/test_abc_classification_profile.py (100%) rename {product_abc_classification_base => product_abc_classification}/tests/test_product.py (100%) rename {product_abc_classification_base => product_abc_classification}/views/abc_classification_product_level.xml (97%) rename {product_abc_classification_base => product_abc_classification}/views/abc_classification_profile.xml (97%) rename {product_abc_classification_base => product_abc_classification}/views/product_product.xml (100%) rename {product_abc_classification_base => product_abc_classification}/views/product_template.xml (100%) create mode 120000 setup/product_abc_classification/odoo/addons/product_abc_classification rename setup/{product_abc_classification_base => product_abc_classification}/setup.py (100%) delete mode 100644 setup/product_abc_classification_base/.eggs/README.txt delete mode 120000 setup/product_abc_classification_base/odoo/addons/product_abc_classification_base diff --git a/product_abc_classification_base/README.rst b/product_abc_classification/README.rst similarity index 100% rename from product_abc_classification_base/README.rst rename to product_abc_classification/README.rst diff --git a/product_abc_classification_base/__init__.py b/product_abc_classification/__init__.py similarity index 100% rename from product_abc_classification_base/__init__.py rename to product_abc_classification/__init__.py diff --git a/product_abc_classification_base/__manifest__.py b/product_abc_classification/__manifest__.py similarity index 100% rename from product_abc_classification_base/__manifest__.py rename to product_abc_classification/__manifest__.py diff --git a/product_abc_classification_base/data/ir_cron.xml b/product_abc_classification/data/ir_cron.xml similarity index 100% rename from product_abc_classification_base/data/ir_cron.xml rename to product_abc_classification/data/ir_cron.xml diff --git a/product_abc_classification_base/i18n/fr.po b/product_abc_classification/i18n/fr.po similarity index 50% rename from product_abc_classification_base/i18n/fr.po rename to product_abc_classification/i18n/fr.po index b311e545b6ea..47bc99abc3fd 100644 --- a/product_abc_classification_base/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_base +# * product_abc_classification # msgid "" msgstr "" @@ -15,313 +15,313 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" -#. module: product_abc_classification_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level_percentage +#. 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level_percentage_products +#. 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_base -#: model:ir.ui.view,arch_db:product_abc_classification_base.product_template_form_view +#. 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_base -#: model:ir.ui.view,arch_db:product_abc_classification_base.abc_classification_product_level_form_view +#. 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_base -#: model:ir.actions.act_window,name:product_abc_classification_base.abc_classification_profile_action -#: model:ir.ui.menu,name:product_abc_classification_base.menu_abc_classification_profile_config_stock +#. 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_base -#: model:ir.ui.view,arch_db:product_abc_classification_base.abc_classification_profile_form_view +#. 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_base -#: model:ir.ui.view,arch_db:product_abc_classification_base.abc_classification_profile_tree_view +#. 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_base -#: model:ir.model,name:product_abc_classification_base.model_abc_classification_product_level +#. 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_base -#: model:ir.model,name:product_abc_classification_base.model_abc_classification_profile +#. 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_base -#: model:ir.ui.view,arch_db:product_abc_classification_base.abc_classification_product_level_search_view +#. 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_delivery_carrier_abc_classification_product_level_ids -#: model:ir.model.fields,field_description:product_abc_classification_base.field_product_product_abc_classification_product_level_ids -#: model:ir.model.fields,field_description:product_abc_classification_base.field_product_template_abc_classification_product_level_ids +#. 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_allowed_profile_ids -#: model:ir.model.fields,field_description:product_abc_classification_base.field_delivery_carrier_abc_classification_profile_ids -#: model:ir.model.fields,field_description:product_abc_classification_base.field_product_product_abc_classification_profile_ids -#: model:ir.model.fields,field_description:product_abc_classification_base.field_product_template_abc_classification_profile_ids +#. 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_base +#. 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_base -#: model:ir.model.fields,help:product_abc_classification_base.field_abc_classification_level_name +#. 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_level_id +#. 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_base -#: code:addons/product_abc_classification_base/models/abc_classification_product_level.py:84 +#. 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_base -#: model:ir.ui.view,arch_db:product_abc_classification_base.abc_classification_product_level_search_view +#. 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_computed_level_id +#. 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_base -#: model:ir.ui.view,arch_db:product_abc_classification_base.abc_classification_product_level_form_view +#. 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_base -#: code:addons/product_abc_classification_base/models/abc_classification_product_level.py:90 +#. 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level_create_uid -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_create_uid -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_create_uid +#. 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level_create_date -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_create_date -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_create_date +#. 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level_display_name -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_display_name -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_display_name +#. 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_base -#: model:ir.ui.view,arch_db:product_abc_classification_base.abc_classification_product_level_search_view +#. 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level_id -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_id -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_id +#. 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_flag +#. 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level___last_update -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level___last_update -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile___last_update +#. 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level_write_uid -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_write_uid -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_write_uid +#. 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level_write_date -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_write_date -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_write_date +#. 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_base -#: model:ir.ui.view,arch_db:product_abc_classification_base.abc_classification_product_level_search_view +#. 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_level_ids +#. 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_base +#. module: product_abc_classification #: sql_constraint:abc.classification.level:0 -#: code:addons/product_abc_classification_base/models/abc_classification_level.py:30 +#: 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_manual_level_id +#. 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_base -#: code:addons/product_abc_classification_base/models/abc_classification_product_level.py:100 +#. 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level_name -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_name +#. 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_base +#. module: product_abc_classification #: sql_constraint:abc.classification.product.level:0 -#: code:addons/product_abc_classification_base/models/abc_classification_product_level.py:76 +#: 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_period +#. 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_base -#: model:ir.model,name:product_abc_classification_base.model_product_product -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_product_id +#. 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_base -#: model:ir.model,name:product_abc_classification_base.model_product_template +#. 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_product_tmpl_id +#. 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_base -#: model:ir.actions.act_window,name:product_abc_classification_base.abc_classification_product_level_action -#: model:ir.ui.menu,name:product_abc_classification_base.menu_abc_classification_product_level_config_stock +#. 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_profile_id -#: model:ir.ui.view,arch_db:product_abc_classification_base.abc_classification_product_level_search_view +#. 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_level_profile_id +#. 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_base +#. module: product_abc_classification #: sql_constraint:abc.classification.profile:0 -#: code:addons/product_abc_classification_base/models/abc_classification_profile.py:33 +#: 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_base -#: code:addons/product_abc_classification_base/models/abc_classification_level.py:39 +#. 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_base -#: code:addons/product_abc_classification_base/models/abc_classification_level.py:51 +#. 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_base -#: code:addons/product_abc_classification_base/models/abc_classification_level.py:55 +#. 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_base -#: code:addons/product_abc_classification_base/models/abc_classification_level.py:43 +#. 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_base -#: code:addons/product_abc_classification_base/models/abc_classification_profile.py:52 +#. 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_base -#: code:addons/product_abc_classification_base/models/abc_classification_profile.py:43 +#. 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_base -#: code:addons/product_abc_classification_base/models/abc_classification_profile.py:60 +#. 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_product_level_profile_type -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_profile_type +#. 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_base -#: model:ir.model.fields,field_description:product_abc_classification_base.field_abc_classification_profile_auto_apply_computed_value +#. 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_base -#: model:ir.model,name:product_abc_classification_base.model_abc_classification_level +#. 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_base/models/__init__.py b/product_abc_classification/models/__init__.py similarity index 100% rename from product_abc_classification_base/models/__init__.py rename to product_abc_classification/models/__init__.py diff --git a/product_abc_classification_base/models/abc_classification_level.py b/product_abc_classification/models/abc_classification_level.py similarity index 100% rename from product_abc_classification_base/models/abc_classification_level.py rename to product_abc_classification/models/abc_classification_level.py diff --git a/product_abc_classification_base/models/abc_classification_product_level.py b/product_abc_classification/models/abc_classification_product_level.py similarity index 100% rename from product_abc_classification_base/models/abc_classification_product_level.py rename to product_abc_classification/models/abc_classification_product_level.py diff --git a/product_abc_classification_base/models/abc_classification_profile.py b/product_abc_classification/models/abc_classification_profile.py similarity index 100% rename from product_abc_classification_base/models/abc_classification_profile.py rename to product_abc_classification/models/abc_classification_profile.py diff --git a/product_abc_classification_base/models/product_product.py b/product_abc_classification/models/product_product.py similarity index 100% rename from product_abc_classification_base/models/product_product.py rename to product_abc_classification/models/product_product.py diff --git a/product_abc_classification_base/models/product_template.py b/product_abc_classification/models/product_template.py similarity index 100% rename from product_abc_classification_base/models/product_template.py rename to product_abc_classification/models/product_template.py diff --git a/product_abc_classification_base/readme/CONTRIBUTORS.rst b/product_abc_classification/readme/CONTRIBUTORS.rst similarity index 100% rename from product_abc_classification_base/readme/CONTRIBUTORS.rst rename to product_abc_classification/readme/CONTRIBUTORS.rst diff --git a/product_abc_classification_base/readme/DESCRIPTION.rst b/product_abc_classification/readme/DESCRIPTION.rst similarity index 100% rename from product_abc_classification_base/readme/DESCRIPTION.rst rename to product_abc_classification/readme/DESCRIPTION.rst diff --git a/product_abc_classification_base/readme/USAGE.rst b/product_abc_classification/readme/USAGE.rst similarity index 100% rename from product_abc_classification_base/readme/USAGE.rst rename to product_abc_classification/readme/USAGE.rst diff --git a/product_abc_classification_base/security/ir.model.access.csv b/product_abc_classification/security/ir.model.access.csv similarity index 100% rename from product_abc_classification_base/security/ir.model.access.csv rename to product_abc_classification/security/ir.model.access.csv diff --git a/product_abc_classification_base/static/description/icon.png b/product_abc_classification/static/description/icon.png similarity index 100% rename from product_abc_classification_base/static/description/icon.png rename to product_abc_classification/static/description/icon.png diff --git a/product_abc_classification_base/static/description/index.html b/product_abc_classification/static/description/index.html similarity index 100% rename from product_abc_classification_base/static/description/index.html rename to product_abc_classification/static/description/index.html diff --git a/product_abc_classification_base/tests/__init__.py b/product_abc_classification/tests/__init__.py similarity index 100% rename from product_abc_classification_base/tests/__init__.py rename to product_abc_classification/tests/__init__.py diff --git a/product_abc_classification_base/tests/common.py b/product_abc_classification/tests/common.py similarity index 100% rename from product_abc_classification_base/tests/common.py rename to product_abc_classification/tests/common.py diff --git a/product_abc_classification_base/tests/test_abc_classification_product_level.py b/product_abc_classification/tests/test_abc_classification_product_level.py similarity index 100% rename from product_abc_classification_base/tests/test_abc_classification_product_level.py rename to product_abc_classification/tests/test_abc_classification_product_level.py diff --git a/product_abc_classification_base/tests/test_abc_classification_profile.py b/product_abc_classification/tests/test_abc_classification_profile.py similarity index 100% rename from product_abc_classification_base/tests/test_abc_classification_profile.py rename to product_abc_classification/tests/test_abc_classification_profile.py diff --git a/product_abc_classification_base/tests/test_product.py b/product_abc_classification/tests/test_product.py similarity index 100% rename from product_abc_classification_base/tests/test_product.py rename to product_abc_classification/tests/test_product.py diff --git a/product_abc_classification_base/views/abc_classification_product_level.xml b/product_abc_classification/views/abc_classification_product_level.xml similarity index 97% rename from product_abc_classification_base/views/abc_classification_product_level.xml rename to product_abc_classification/views/abc_classification_product_level.xml index e6332542fe8f..71ac10979bd2 100644 --- a/product_abc_classification_base/views/abc_classification_product_level.xml +++ b/product_abc_classification/views/abc_classification_product_level.xml @@ -5,7 +5,7 @@ abc.classification.product.level.form (in product_abc_classification_base) + >abc.classification.product.level.form (in product_abc_classification)
    abc.classification.product.level @@ -41,7 +41,7 @@ abc.classification.product.level.tree (in product_abc_classification_base) + >abc.classification.product.level.tree (in product_abc_classification) abc.classification.product.level @@ -56,7 +56,7 @@ abc.classification.product.level.search (in product_abc_classification_base) + >abc.classification.product.level.search (in product_abc_classification) abc.classification.product.level diff --git a/product_abc_classification_base/views/abc_classification_profile.xml b/product_abc_classification/views/abc_classification_profile.xml similarity index 97% rename from product_abc_classification_base/views/abc_classification_profile.xml rename to product_abc_classification/views/abc_classification_profile.xml index 781f0cd122c8..f7eaf1f93dab 100644 --- a/product_abc_classification_base/views/abc_classification_profile.xml +++ b/product_abc_classification/views/abc_classification_profile.xml @@ -5,7 +5,7 @@ abc.classification.profile.form (in product_abc_classification_base) + >abc.classification.profile.form (in product_abc_classification) abc.classification.profile @@ -32,7 +32,7 @@ abc.classification.profile.tree (in product_abc_classification_base) + >abc.classification.profile.tree (in product_abc_classification) abc.classification.profile diff --git a/product_abc_classification_base/views/product_product.xml b/product_abc_classification/views/product_product.xml similarity index 100% rename from product_abc_classification_base/views/product_product.xml rename to product_abc_classification/views/product_product.xml diff --git a/product_abc_classification_base/views/product_template.xml b/product_abc_classification/views/product_template.xml similarity index 100% rename from product_abc_classification_base/views/product_template.xml rename to product_abc_classification/views/product_template.xml diff --git a/setup/product_abc_classification/odoo/addons/product_abc_classification b/setup/product_abc_classification/odoo/addons/product_abc_classification new file mode 120000 index 000000000000..8571d22e4ad8 --- /dev/null +++ b/setup/product_abc_classification/odoo/addons/product_abc_classification @@ -0,0 +1 @@ +../../../../product_abc_classification \ No newline at end of file diff --git a/setup/product_abc_classification_base/setup.py b/setup/product_abc_classification/setup.py similarity index 100% rename from setup/product_abc_classification_base/setup.py rename to setup/product_abc_classification/setup.py diff --git a/setup/product_abc_classification_base/.eggs/README.txt b/setup/product_abc_classification_base/.eggs/README.txt deleted file mode 100644 index 5d01668824f4..000000000000 --- a/setup/product_abc_classification_base/.eggs/README.txt +++ /dev/null @@ -1,6 +0,0 @@ -This directory contains eggs that were downloaded by setuptools to build, test, and run plug-ins. - -This directory caches those eggs to prevent repeated downloads. - -However, it is safe to delete this directory. - diff --git a/setup/product_abc_classification_base/odoo/addons/product_abc_classification_base b/setup/product_abc_classification_base/odoo/addons/product_abc_classification_base deleted file mode 120000 index ddbb5a97873a..000000000000 --- a/setup/product_abc_classification_base/odoo/addons/product_abc_classification_base +++ /dev/null @@ -1 +0,0 @@ -../../../../product_abc_classification_base \ No newline at end of file From 67d62bdda8e8762f09be940cc23a3c8c6a7357b9 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 15/21] [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 b57b0a1e46d7..6411834d0ee2 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 f7eaf1f93dab..65830d78c4fc 100644 --- a/product_abc_classification/views/abc_classification_profile.xml +++ b/product_abc_classification/views/abc_classification_profile.xml @@ -10,6 +10,16 @@ +
    + +
    From 217adcc1447cda1ca53c768056c08387399edbe6 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Tue, 15 Nov 2022 08:13:45 +0100 Subject: [PATCH 16/21] [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 ef810e32598a..b6b19dc26ca6 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 1127c274e5fb..2f2873568fc7 100644 --- a/product_abc_classification/views/product_template.xml +++ b/product_abc_classification/views/product_template.xml @@ -18,7 +18,7 @@ From b636e384f303d20183fe29216cdf50ff3377d8cb Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Thu, 17 Nov 2022 14:23:31 +0100 Subject: [PATCH 17/21] [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 6411834d0ee2..c71f4b4bb63b 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 923ae1eefe9b..add08143a4a3 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 91a0fcbaa77275dc18c301027bd475cc6d43d671 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Tue, 22 Nov 2022 10:47:00 +0100 Subject: [PATCH 18/21] [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 1ad9cf6c1843..7def032e335b 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 8cfa5f2ce0b4eba02d5ce9985d5df93ce2b7228e Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Wed, 15 Feb 2023 12:51:41 +0100 Subject: [PATCH 19/21] [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 c71f4b4bb63b..dc7fd29f3ad8 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 65830d78c4fc..89b078b40dac 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 20/21] [FIX] product_abc_classification: Translated terms into views --- product_abc_classification/i18n/fr.po | 22 +++++++++++----------- requirements.txt | 2 -- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/product_abc_classification/i18n/fr.po b/product_abc_classification/i18n/fr.po index 47bc99abc3fd..dffcbb0a26ca 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" diff --git a/requirements.txt b/requirements.txt index dbf0088e1146..180fc49789ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,2 @@ # generated from manifests external_dependencies openupgradelib -freezegun==0.3.14 - From 70beefc48f634a102844592f3f1ceb91944cff95 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Mon, 18 Sep 2023 11:43:52 +0200 Subject: [PATCH 21/21] [FIX] product_abc_classification: Fix tests --- product_abc_classification/tests/common.py | 7 ++++++- .../tests/test_abc_classification_profile.py | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/product_abc_classification/tests/common.py b/product_abc_classification/tests/common.py index 4900770fdce2..d4c24f0cdf40 100644 --- a/product_abc_classification/tests/common.py +++ b/product_abc_classification/tests/common.py @@ -1,6 +1,7 @@ # Copyright 2021 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo.fields import Command from odoo.tests.common import TransactionCase @@ -118,6 +119,10 @@ 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)], + "product_template_attribute_value_ids": [ + Command.set( + size_value.pav_attribute_line_ids.product_template_value_ids.ids + ) + ], } ) diff --git a/product_abc_classification/tests/test_abc_classification_profile.py b/product_abc_classification/tests/test_abc_classification_profile.py index 51691913cf2b..68044ba97bfe 100644 --- a/product_abc_classification/tests/test_abc_classification_profile.py +++ b/product_abc_classification/tests/test_abc_classification_profile.py @@ -4,6 +4,7 @@ from psycopg2 import IntegrityError from odoo.exceptions import ValidationError +from odoo.tools.misc import mute_logger from .common import ABCClassificationCase @@ -185,6 +186,7 @@ def test_04(self): } ) + @mute_logger("odoo.sql_db") def test_05(self): """ Data: @@ -282,6 +284,7 @@ def test_06(self): ) self.assertTrue(new_profile) + @mute_logger("odoo.sql_db") def test_07(self): """ Data: