diff --git a/addons/product/populate/__init__.py b/addons/product/populate/__init__.py index 3d587b163e9..781adda49a4 100644 --- a/addons/product/populate/__init__.py +++ b/addons/product/populate/__init__.py @@ -2,3 +2,5 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import product +from . import product_template +from . import product_pricelist diff --git a/addons/product/populate/product.py b/addons/product/populate/product.py index e1a4e1dfcec..6e9367e2f02 100644 --- a/addons/product/populate/product.py +++ b/addons/product/populate/product.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging +import collections from odoo import models from odoo.tools import populate @@ -11,40 +12,84 @@ _logger = logging.getLogger(__name__) class ProductCategory(models.Model): _inherit = "product.category" - _populate_sizes = {"small": 50, "medium": 500, "large": 30000} + _populate_sizes = {"small": 50, "medium": 500, "large": 5_000} def _populate_factories(self): - # quid of parent_id ??? + return [("name", populate.constant('PC_{counter}'))] - return [("name", populate.constant('product_category_{counter}'))] + def _populate(self, size): + categories = super()._populate(size) + # Set parent/child relation + self._populate_set_parents(categories, size) + return categories + def _populate_set_parents(self, categories, size): + _logger.info('Set parent/child relation of product categories') + parent_ids = [] + rand = populate.Random('product.category+parent_generator') + + for category in categories: + if rand.random() < 0.25: + parent_ids.append(category.id) + + categories -= self.browse(parent_ids) # Avoid recursion in parent-child relations. + parent_childs = collections.defaultdict(lambda: self.env['product.category']) + for category in categories: + if rand.random() < 0.25: # 1/4 of remaining categories have a parent. + parent_childs[rand.choice(parent_ids)] |= category + + for count, (parent, children) in enumerate(parent_childs.items()): + if (count + 1) % 1000 == 0: + _logger.info('Setting parent: %s/%s', count + 1, len(parent_childs)) + children.write({'parent_id': parent}) class ProductProduct(models.Model): _inherit = "product.product" - _populate_sizes = {"small": 150, "medium": 5000, "large": 60000} + _populate_sizes = {"small": 150, "medium": 5_000, "large": 50_000} + _populate_dependencies = ["product.category"] - def _populate_factories(self): + def _populate_get_types(self): + return ["consu", "service"], [2, 1] + + def _populate_get_product_factories(self): + category_ids = self.env.registry.populated_models["product.category"] + types, types_distribution = self._populate_get_types() + + def get_rand_float(values, counter, random): + return random.randrange(0, 1500) * random.random() + + # TODO sale & purchase uoms return [ - ("name", populate.constant('product_template_name_{counter}')), ("sequence", populate.randomize([False] + [i for i in range(1, 101)])), - ("description", populate.constant('product_template_description_{counter}')), - ("default_code", populate.constant('product_default_code_{counter}')), ("active", populate.randomize([True, False], [0.8, 0.2])), + ("type", populate.randomize(types, types_distribution)), + ("categ_id", populate.randomize(category_ids)), + ("lst_price", populate.compute(get_rand_float)), + ("standard_price", populate.compute(get_rand_float)), ] + def _populate_factories(self): + return [ + ("name", populate.constant('product_product_name_{counter}')), + ("description", populate.constant('product_product_description_{counter}')), + ("default_code", populate.constant('PP-{counter}')), + ("barcode", populate.constant('BARCODE-PP-{counter}')), + ] + self._populate_get_product_factories() + class SupplierInfo(models.Model): _inherit = 'product.supplierinfo' _populate_sizes = {'small': 450, 'medium': 15_000, 'large': 180_000} - _populate_dependencies = ['res.partner'] + _populate_dependencies = ['res.partner', 'product.product', 'product.template'] def _populate_factories(self): random = populate.Random('product_with_supplierinfo') company_ids = self.env.registry.populated_models['res.company'][:COMPANY_NB_WITH_STOCK] + [False] partner_ids = self.env.registry.populated_models['res.partner'] product_templates_ids = self.env['product.product'].browse(self.env.registry.populated_models['product.product']).product_tmpl_id.ids + product_templates_ids += self.env.registry.populated_models['product.template'] product_templates_ids = random.sample(product_templates_ids, int(len(product_templates_ids) * 0.95)) def get_company_id(values, counter, random): diff --git a/addons/product/populate/product_pricelist.py b/addons/product/populate/product_pricelist.py new file mode 100644 index 00000000000..22a541933bb --- /dev/null +++ b/addons/product/populate/product_pricelist.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models +from odoo.tools import populate +from datetime import timedelta, date + + +class Pricelist(models.Model): + _inherit = "product.pricelist" + _populate_sizes = {"small": 20, "medium": 100, "large": 1_500} + _populate_dependencies = ["res.company"] + + def _populate(self, size): + + # Reflect the settings with data created + self.env['res.config.settings'].create({ + 'group_product_pricelist': True, # Activate pricelist + 'group_sale_pricelist': True, # Activate advanced pricelist + }).execute() + + return super()._populate(size) + + def _populate_factories(self): + company_ids = self.env.registry.populated_models["res.company"] + + return [ + ("company_id", populate.iterate(company_ids + [False for i in range(len(company_ids))])), + ("name", populate.constant('product_pricelist_{counter}')), + ("currency_id", populate.randomize(self.env["res.currency"].search([("active", "=", True)]).ids)), + ("sequence", populate.randomize([False] + [i for i in range(1, 101)])), + ("discount_policy", populate.randomize(["with_discount", "without_discount"])), + ("active", populate.randomize([True, False], [0.8, 0.2])), + ] + + +class PricelistItem(models.Model): + _inherit = "product.pricelist.item" + _populate_sizes = {"small": 500, "medium": 5_000, "large": 50_000} + _populate_dependencies = ["product.product", "product.template", "product.pricelist"] + + def _populate_factories(self): + pricelist_ids = self.env.registry.populated_models["product.pricelist"] + product_ids = self.env.registry.populated_models["product.product"] + p_tmpl_ids = self.env.registry.populated_models["product.template"] + categ_ids = self.env.registry.populated_models["product.category"] + + def get_target_info(iterator, field_name, model_name): + random = populate.Random("pricelist_target") + for values in iterator: + # If product population is updated to consider multi company + # the company of product would have to be considered + # for product_id & product_tmpl_id + applied_on = values["applied_on"] + if applied_on == "0_product_variant": + values["product_id"] = random.choice(product_ids) + elif applied_on == "1_product": + values["product_tmpl_id"] = random.choice(p_tmpl_ids) + elif applied_on == "2_product_category": + values["categ_id"] = random.choice(categ_ids) + yield values + + def get_prices(iterator, field_name, model_name): + random = populate.Random("pricelist_prices") + for values in iterator: + # Fixed price, percentage, formula + compute_price = values["compute_price"] + if compute_price == "fixed": + # base = "list_price" = default + # fixed_price + values["fixed_price"] = random.randint(1, 1000) + elif compute_price == "percentage": + # base = "list_price" = default + # percent_price + values["percent_price"] = random.randint(1, 100) + else: # formula + # pricelist base not considered atm. + values["base"] = random.choice(["list_price", "standard_price"]) + values["price_discount"] = random.randint(0, 100) + # price_min_margin, price_max_margin + # price_round ??? price_discount, price_surcharge + yield values + + now = date.today() + + def get_date_start(values, counter, random): + if random.random() > 0.5: # 50 % of chance to have validation dates + return now + timedelta(days=random.randint(-20, 20)) + else: + False + + def get_date_end(values, counter, random): + if values['date_start']: # 50 % of chance to have validation dates + return values['date_start'] + timedelta(days=random.randint(5, 100)) + else: + False + + return [ + ("pricelist_id", populate.randomize(pricelist_ids)), + ("applied_on", populate.randomize( + ["3_global", "2_product_category", "1_product", "0_product_variant"], + [5, 3, 2, 1], + )), + ("compute_price", populate.randomize( + ["fixed", "percentage", "formula"], + [5, 3, 1], + )), + ("_price", get_prices), + ("_target", get_target_info), + ("min_quantity", populate.randint(0, 50)), + ("date_start", populate.compute(get_date_start)), + ("date_end", populate.compute(get_date_end)), + ] diff --git a/addons/product/populate/product_template.py b/addons/product/populate/product_template.py new file mode 100644 index 00000000000..3f250880885 --- /dev/null +++ b/addons/product/populate/product_template.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import logging +from collections import defaultdict +from functools import reduce + +from odoo import models +from odoo.tools import populate + +_logger = logging.getLogger(__name__) + + +class ProductAttribute(models.Model): + _inherit = "product.attribute" + _populate_sizes = {"small": 20, "medium": 150, "large": 750} + + def _populate(self, size): + + # Reflect the settings with data created + self.env['res.config.settings'].create({ + 'group_product_variant': True, # Activate variant + }).execute() + + return super()._populate(size) + + def _populate_factories(self): + return [ + ("name", populate.constant('PA_{counter}')), + ("sequence", populate.randomize([False] + [i for i in range(1, 101)])), + ("create_variant", populate.randomize(["always", "dynamic", "no_variant"])), + ] + + +class ProductAttributeValue(models.Model): + _inherit = "product.attribute.value" + _populate_dependencies = ["product.attribute"] + _populate_sizes = {"small": 100, "medium": 1_000, "large": 10_000} + + def _populate_factories(self): + attribute_ids = self.env.registry.populated_models["product.attribute"] + + return [ + ("name", populate.constant('PAV_{counter}')), + ("sequence", populate.randomize([False] + [i for i in range(1, 101)])), + ("attribute_id", populate.randomize(attribute_ids)), + ] + + +class ProductTemplate(models.Model): + _inherit = "product.template" + _populate_sizes = {"small": 150, "medium": 5_000, "large": 50_000} + _populate_dependencies = ["product.attribute.value", "product.category"] + + def _populate(self, size): + res = super()._populate(size) + + def set_barcode_variant(sample_ratio): + random = populate.Random('barcode_product_template') + product_variants_ids = res.product_variant_ids.ids + product_variants_ids = random.sample(product_variants_ids, int(len(product_variants_ids) * sample_ratio)) + product_variants = self.env['product.product'].browse(product_variants_ids) + _logger.info('Set barcode on product variants (%s)', len(product_variants)) + for product in product_variants: + product.barcode = "BARCODE-PT-%s" % product.id + + set_barcode_variant(0.85) + + return res + + def _populate_factories(self): + attribute_ids = self.env.registry.populated_models["product.attribute"] + attribute_ids_by_types = defaultdict(list) + attributes = self.env["product.attribute"].browse(attribute_ids) + for attr in attributes: + attribute_ids_by_types[attr.create_variant].append(attr.id) + + def get_attributes(values, counter, random): + if random.random() < 0.20: # 20 % chance to have no attributes + return False + attributes_qty = random.choices( + [1, 2, 3, 4, 5, 6, 8, 10], + [10, 9, 8, 7, 6, 4, 1, 0.5], + )[0] + attr_line_vals = [] + attribute_used_ids = attribute_ids + if random.random() < 0.20: # 20 % chance of using only always attributes (to test when product has lot of variant) + attribute_used_ids = attribute_ids_by_types["always"] + + no_variant = False + values_count = [0 for i in range(attributes_qty)] + + def will_exceed(i): + return not no_variant and reduce((lambda x, y: (x or 1) * (y or 1)), values_count[i:] + [values_count[i] + 1] + values_count[:i]) > 1000 + + for i in range(attributes_qty): + if will_exceed(i): + return attr_line_vals + attr_id = random.choice(attribute_used_ids) + attr = self.env["product.attribute"].browse(attr_id) + if attr.create_variant == "dynamic": + no_variant = True + if not attr.value_ids: + # attribute without any value + continue + nb_values = len(attr.value_ids) + vals_qty = random.randrange(nb_values) + 1 + value_ids = set() + for __ in range(vals_qty): + # Ensure that we wouldn't have > 1k variants with the generated attributes combination + if will_exceed(i): + break + random_value_id = attr.value_ids[random.randrange(nb_values)].id + if random_value_id not in value_ids: + values_count[i] += 1 + value_ids.add(random_value_id) + + attr_line_vals.append((0, 0, { + "attribute_id": attr_id, + "value_ids": [(6, 0, list(value_ids))], + })) + + return attr_line_vals + + return [ + ("name", populate.constant('product_template_name_{counter}')), + ("description", populate.constant('product_template_description_{counter}')), + ("default_code", populate.constant('PT-{counter}')), + ("attribute_line_ids", populate.compute(get_attributes)), + ] + self.env['product.product']._populate_get_product_factories() + + +class ProductTemplateAttributeExclusion(models.Model): + _inherit = "product.template.attribute.exclusion" + _populate_dependencies = ["product.template"] + _populate_sizes = {"small": 200, "medium": 1_000, "large": 5_000} + + def _populate_factories(self): + p_tmpl_ids = self.env.registry.populated_models["product.template"] + + configurable_templates = self.env["product.template"].search([ + ('id', 'in', p_tmpl_ids), + ('has_configurable_attributes', '=', True), + ]) + tmpl_ids_possible = [] + multi_values_attribute_lines_by_tmpl = {} + for template in configurable_templates: + multi_values_attribute_lines = template.attribute_line_ids.filtered( + lambda l: len(l.value_ids) > 1 + ) + if len(multi_values_attribute_lines) < 2: + continue + tmpl_ids_possible.append(template.id) + multi_values_attribute_lines_by_tmpl[template.id] = multi_values_attribute_lines + + def get_product_template_attribute_value_id(values, counter, random): + return random.choice(multi_values_attribute_lines_by_tmpl[values['product_tmpl_id']].product_template_value_ids.ids) + + def get_value_ids(values, counter, random): + attr_val = self.env['product.template.attribute.value'].browse(values['product_template_attribute_value_id']).attribute_line_id + remaining_lines = multi_values_attribute_lines_by_tmpl[values['product_tmpl_id']] - attr_val + return [( + # TODO: multiple values + 6, 0, [random.choice(remaining_lines.product_template_value_ids).id] + )] + + return [ + ("product_tmpl_id", populate.randomize(tmpl_ids_possible)), + ("product_template_attribute_value_id", populate.compute(get_product_template_attribute_value_id)), + ("value_ids", populate.compute(get_value_ids)), + ] + + +class ProductTemplateAttributeValue(models.Model): + _inherit = "product.template.attribute.value" + _populate_dependencies = ["product.template"] + + def _populate(self, size): + p_tmpl_ids = self.env.registry.populated_models["product.template"] + ptavs = self.search([('product_tmpl_id', 'in', p_tmpl_ids)]) + # ptavs are automatically created when specifying attribute lines on product templates. + + rand = populate.Random("ptav_extra_price_generator") + for ptav in ptavs: + if rand.random() < 0.50: # 50% of having a extra price + ptav.price_extra = rand.randrange(500) * rand.random() + + return ptavs diff --git a/addons/stock/populate/__init__.py b/addons/stock/populate/__init__.py index d758736382c..c948781b3be 100644 --- a/addons/stock/populate/__init__.py +++ b/addons/stock/populate/__init__.py @@ -2,3 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import stock +from . import product diff --git a/addons/stock/populate/product.py b/addons/stock/populate/product.py new file mode 100644 index 00000000000..495df69391c --- /dev/null +++ b/addons/stock/populate/product.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models +from odoo.tools import populate + + +class ProductProduct(models.Model): + _inherit = 'product.product' + + def _populate_get_types(self): + types, types_distribution = super()._populate_get_types() + return types + ["product"], types_distribution + [3] + + def _populate_factories(self): + + def get_tracking(values, counter, random): + if values['type'] == 'product': + return random.choices(['none', 'lot', 'serial'], [0.7, 0.2, 0.1])[0] + else: + return 'none' + + return super()._populate_factories() + [ + ('tracking', populate.compute(get_tracking)) + ] diff --git a/addons/stock/populate/stock.py b/addons/stock/populate/stock.py index b97eac92922..2a82f500bb5 100644 --- a/addons/stock/populate/stock.py +++ b/addons/stock/populate/stock.py @@ -16,23 +16,6 @@ _logger = logging.getLogger(__name__) COMPANY_NB_WITH_STOCK = 3 # Need to be smaller than 5 (_populate_sizes['small'] of company) -class ProductProduct(models.Model): - _inherit = 'product.product' - - def _populate_factories(self): - - def get_tracking(values, counter, random): - if values['type'] == 'product': - return random.choices(['none', 'lot', 'serial'], [0.7, 0.2, 0.1])[0] - else: - return 'none' - - res = super()._populate_factories() - res.append(('type', populate.iterate(['consu', 'service', 'product'], [0.3, 0.2, 0.5]))) - res.append(('tracking', populate.compute(get_tracking))) - return res - - class Warehouse(models.Model): _inherit = 'stock.warehouse'