diff --git a/.tx/config b/.tx/config index b132de9a8f9..62c7357f296 100644 --- a/.tx/config +++ b/.tx/config @@ -942,11 +942,6 @@ file_filter = addons/website_sale_management/i18n/.po source_file = addons/website_sale_management/i18n/website_sale_management.pot source_lang = en -[odoo-12.website_sale_options] -file_filter = addons/website_sale_options/i18n/.po -source_file = addons/website_sale_options/i18n/website_sale_options.pot -source_lang = en - [odoo-12.website_sale_stock] file_filter = addons/website_sale_stock/i18n/.po source_file = addons/website_sale_stock/i18n/website_sale_stock.pot diff --git a/addons/point_of_sale/tests/test_frontend.py b/addons/point_of_sale/tests/test_frontend.py index 6f604e13451..64dba64d84b 100644 --- a/addons/point_of_sale/tests/test_frontend.py +++ b/addons/point_of_sale/tests/test_frontend.py @@ -31,15 +31,14 @@ class TestUi(odoo.tests.HttpCase): pear = env.ref('point_of_sale.whiteboard') attribute_value = env['product.attribute.value'].create({ 'name': 'add 2', - 'product_ids': [(6, 0, [pear.id])], 'attribute_id': env['product.attribute'].create({ 'name': 'add 2', }).id, }) - env['product.attribute.price'].create({ + env['product.product.attribute.value'].create({ 'product_tmpl_id': pear.product_tmpl_id.id, 'price_extra': 2, - 'value_id': attribute_value.id, + 'product_attribute_value_id': attribute_value.id, }) fixed_pricelist = env['product.pricelist'].create({ diff --git a/addons/product/data/product_demo.xml b/addons/product/data/product_demo.xml index dd730d92904..0a2ac250e55 100644 --- a/addons/product/data/product_demo.xml +++ b/addons/product/data/product_demo.xml @@ -212,16 +212,6 @@ - - - - - - - - 50.40 - - Corner Desk Right Sit @@ -320,14 +310,34 @@ - + - - - + + + + + + + 50.40 + + + 6.40 diff --git a/addons/product/models/product.py b/addons/product/models/product.py index 3318c9a075a..78cb392dcf2 100644 --- a/addons/product/models/product.py +++ b/addons/product/models/product.py @@ -93,7 +93,7 @@ class ProductProduct(models.Model): lst_price = fields.Float( 'Sale Price', compute='_compute_product_lst_price', digits=dp.get_precision('Product Price'), inverse='_set_product_lst_price', - help="The sale price is managed from the product template. Click on the 'Variant Prices' button to set the extra attribute prices.") + help="The sale price is managed from the product template. Click on the 'Configure Variants' button to set the extra attribute prices.") default_code = fields.Char('Internal Reference', index=True) code = fields.Char('Reference', compute='_compute_product_code') @@ -110,6 +110,8 @@ class ProductProduct(models.Model): help="International Article Number used for product identification.") attribute_value_ids = fields.Many2many( 'product.attribute.value', string='Attributes', ondelete='restrict') + product_attribute_value_ids = fields.Many2many( + 'product.product.attribute.value', string='Attribute Values', compute="_compute_product_attribute_value_ids") # image: all image fields are base64 encoded and PIL-supported image_variant = fields.Binary( "Variant Image", attachment=True, @@ -193,15 +195,10 @@ class ProductProduct(models.Model): value -= product.price_extra product.write({'list_price': value}) - @api.depends('attribute_value_ids.price_ids.price_extra', 'attribute_value_ids.price_ids.product_tmpl_id') + @api.depends('product_attribute_value_ids.price_extra') def _compute_product_price_extra(self): - # TDE FIXME: do a real multi and optimize a bit ? for product in self: - price_extra = 0.0 - for attribute_price in product.mapped('attribute_value_ids.price_ids'): - if attribute_price.product_tmpl_id == product.product_tmpl_id: - price_extra += attribute_price.price_extra - product.price_extra = price_extra + product.price_extra = sum(product.mapped('product_attribute_value_ids.price_extra')) @api.depends('list_price', 'price_extra') def _compute_product_lst_price(self): @@ -276,6 +273,12 @@ class ProductProduct(models.Model): else: self.product_tmpl_id.image = image + def _compute_product_attribute_value_ids(self): + for product in self: + product.product_attribute_value_ids = self.env['product.product.attribute.value']._search([ + ('product_tmpl_id', '=', product.product_tmpl_id.id), + ('product_attribute_value_id', 'in', product.attribute_value_ids.ids)]) + @api.one def _get_pricelist_items(self): self.pricelist_item_ids = self.env['product.pricelist.item'].search([ diff --git a/addons/product/models/product_attribute.py b/addons/product/models/product_attribute.py index e2c20145455..0a348370199 100644 --- a/addons/product/models/product_attribute.py +++ b/addons/product/models/product_attribute.py @@ -19,75 +19,67 @@ class ProductAttribute(models.Model): create_variant = fields.Boolean(default=True, help="Check this if you want to create multiple variants for this attribute.") -class ProductAttributevalue(models.Model): +class ProductAttributeValue(models.Model): _name = "product.attribute.value" _order = 'attribute_id, sequence, id' _description = 'Product Attribute Value' - name = fields.Char('Attribute Value', required=True, translate=True) - sequence = fields.Integer('Sequence', help="Determine the display order") - attribute_id = fields.Many2one('product.attribute', 'Attribute', ondelete='cascade', required=True) - product_ids = fields.Many2many('product.product', string='Variants', readonly=True) - price_extra = fields.Float( - 'Attribute Price Extra', compute='_compute_price_extra', inverse='_set_price_extra', - default=0.0, digits=dp.get_precision('Product Price'), - help="Price Extra: Extra price for the variant with this attribute value on sale price. eg. 200 price extra, 1000 + 200 = 1200.") - price_ids = fields.One2many('product.attribute.price', 'value_id', 'Attribute Prices', readonly=True) + name = fields.Char(string='Value', required=True, translate=True) + sequence = fields.Integer(string='Sequence', help="Determine the display order") + attribute_id = fields.Many2one('product.attribute', string='Attribute', ondelete='cascade', required=True) _sql_constraints = [ - ('value_company_uniq', 'unique (name,attribute_id)', 'This attribute value already exists !') + ('value_company_uniq', 'unique (name, attribute_id)', 'This attribute value already exists !') ] - @api.one - def _compute_price_extra(self): - if self._context.get('active_id'): - price = self.price_ids.filtered(lambda price: price.product_tmpl_id.id == self._context['active_id']) - self.price_extra = price.price_extra - else: - self.price_extra = 0.0 - - def _set_price_extra(self): - if not self._context.get('active_id'): - return - - AttributePrice = self.env['product.attribute.price'] - prices = AttributePrice.search([('value_id', 'in', self.ids), ('product_tmpl_id', '=', self._context['active_id'])]) - updated = prices.mapped('value_id') - if prices: - prices.write({'price_extra': self.price_extra}) - else: - for value in self - updated: - AttributePrice.create({ - 'product_tmpl_id': self._context['active_id'], - 'value_id': value.id, - 'price_extra': self.price_extra, - }) - @api.multi def name_get(self): if not self._context.get('show_attribute', True): # TDE FIXME: not used - return super(ProductAttributevalue, self).name_get() + return super(ProductAttributeValue, self).name_get() return [(value.id, "%s: %s" % (value.attribute_id.name, value.name)) for value in self] + @api.multi + def _variant_name(self, variable_attributes): + return ", ".join([v.name for v in self if v.attribute_id in variable_attributes]) + @api.multi def unlink(self): linked_products = self.env['product.product'].with_context(active_test=False).search([('attribute_value_ids', 'in', self.ids)]) if linked_products: raise UserError(_('The operation cannot be completed:\nYou are trying to delete an attribute value with a reference on a product variant.')) - return super(ProductAttributevalue, self).unlink() + return super(ProductAttributeValue, self).unlink() + + +class ProductProductAttributeValue(models.Model): + _name = "product.product.attribute.value" + + name = fields.Char('Value', related="product_attribute_value_id.name") + product_attribute_value_id = fields.Many2one('product.attribute.value', string='Attribute Value', required=True, ondelete='cascade', index=True) + product_tmpl_id = fields.Many2one('product.template', string='Product Template', required=True, ondelete='cascade', index=True) + attribute_id = fields.Many2one('product.attribute', string='Attribute', related="product_attribute_value_id.attribute_id") + sequence = fields.Integer('product.sequence', related="product_attribute_value_id.sequence") + price_extra = fields.Float( + string='Attribute Price Extra', default=0.0, digits=dp.get_precision('Product Price'), + help="Price Extra: Extra price for the variant with this attribute value on sale price. eg. 200 price extra, 1000 + 200 = 1200.") + exclude_for = fields.Many2many( + 'product.attribute.filter.line', string="Exclude for", relation="product_attribute_value_exclusion", + help="""A list of product and attribute values that you want to exclude for this product's attribue value. + Also applies on optionnal and accessory products.""") @api.multi - def _variant_name(self, variable_attributes): - return ", ".join([v.name for v in self if v.attribute_id in variable_attributes]) + def name_get(self): + if not self._context.get('show_attribute', True): # TDE FIXME: not used + return super(ProductAttributeValue, self).name_get() + return [(value.id, "%s: %s" % (value.attribute_id.name, value.name)) for value in self] -class ProductAttributePrice(models.Model): - _name = "product.attribute.price" - _description = 'Product Attribute Price' +class ProductAttributeFilterLine(models.Model): + _name = "product.attribute.filter.line" - product_tmpl_id = fields.Many2one('product.template', 'Product Template', ondelete='cascade', required=True) - value_id = fields.Many2one('product.attribute.value', 'Product Attribute Value', ondelete='cascade', required=True) - price_extra = fields.Float('Price Extra', digits=dp.get_precision('Product Price')) + product_tmpl_id = fields.Many2one('product.template', string='Product Template', ondelete='cascade', required=True) + value_ids = fields.Many2many( + 'product.product.attribute.value', relation="product_attr_filter_line_value_ids_rel", + string='Attribute Values', domain="[('product_tmpl_id', '=', product_tmpl_id), ('attribute_id.create_variant', '=', True)]") class ProductAttributeLine(models.Model): @@ -95,9 +87,10 @@ class ProductAttributeLine(models.Model): _rec_name = 'attribute_id' _description = 'Product Attribute Line' - product_tmpl_id = fields.Many2one('product.template', 'Product Template', ondelete='cascade', required=True) - attribute_id = fields.Many2one('product.attribute', 'Attribute', ondelete='restrict', required=True) + product_tmpl_id = fields.Many2one('product.template', string='Product Template', ondelete='cascade', required=True) + attribute_id = fields.Many2one('product.attribute', string='Attribute', ondelete='restrict', required=True) value_ids = fields.Many2many('product.attribute.value', string='Attribute Values') + product_value_ids = fields.Many2many('product.product.attribute.value', string='Product Attribute Values', compute="_set_product_value_ids") @api.constrains('value_ids', 'attribute_id') def _check_valid_attribute(self): @@ -105,6 +98,64 @@ class ProductAttributeLine(models.Model): raise ValidationError(_('You cannot use this attribute with the following value.')) return True + @api.model + def create(self, values): + res = super(ProductAttributeLine, self).create(values) + res._update_product_product_attribute_values() + return res + + def write(self, values): + res = super(ProductAttributeLine, self).write(values) + self._update_product_product_attribute_values() + return res + + @api.depends('value_ids') + def _set_product_value_ids(self): + for product_attribute_line in self: + product_attribute_line.product_value_ids = self.env['product.product.attribute.value'].search([ + ('product_tmpl_id', 'in', product_attribute_line.product_tmpl_id.ids), + ('product_attribute_value_id.attribute_id', 'in', product_attribute_line.value_ids.mapped('attribute_id').ids)]) + + @api.multi + def unlink(self): + for product_attribute_line in self: + self.env['product.product.attribute.value'].search([ + ('product_tmpl_id', 'in', product_attribute_line.product_tmpl_id.ids), + ('product_attribute_value_id.attribute_id', 'in', product_attribute_line.value_ids.mapped('attribute_id').ids)]).unlink() + + return super(ProductAttributeLine, self).unlink() + + def _update_product_product_attribute_values(self): + """ + Create or unlink product.product.attribute.value based on the attribute lines. + If the product.attribute.value is removed, remove the corresponding product.product.attribute.value + If no product.product.attribute.value exists for the newly added product.attribute.value, create it. + """ + for attribute_line in self: + # All existing product.product.attribute.value for this template + product_product_attribute_values_to_remove = self.env['product.product.attribute.value'].search([ + ('product_tmpl_id', '=', attribute_line.product_tmpl_id.id), + ('product_attribute_value_id.attribute_id', 'in', attribute_line.value_ids.mapped('attribute_id').ids)]) + # All existing product.attribute.value shared by all products + # eg (Yellow, Red, Blue, Small, Large) + existing_product_attribute_values = product_product_attribute_values_to_remove.mapped('product_attribute_value_id') + + # Loop on product.attribute.values for the line (eg: Yellow, Red, Blue) + for product_attribute_value in attribute_line.value_ids: + if product_attribute_value in existing_product_attribute_values: + # property is already existing: don't touch, remove it from list to avoid unlinking it + product_product_attribute_values_to_remove = product_product_attribute_values_to_remove.filtered( + lambda value: product_attribute_value not in value.mapped('product_attribute_value_id') + ) + else: + # property does not exist: create it + self.env['product.product.attribute.value'].create({ + 'product_attribute_value_id': product_attribute_value.id, + 'product_tmpl_id': attribute_line.product_tmpl_id.id}) + # at this point, existing properties can be removed to reflect the modifications on value_ids + if product_product_attribute_values_to_remove: + product_product_attribute_values_to_remove.unlink() + @api.model def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None): # TDE FIXME: currently overriding the domain; however as it includes a diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index e035e68cffe..2255403b4df 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -479,6 +479,59 @@ class ProductTemplate(models.Model): return True + @api.multi + def get_filtered_variants(self, reference_product=None): + """ + Will filter availability (excluded) for the product + combinations (ex: color: white excludes size: large). + + Will also filter availability (excluded) for the parent + product if specified (meaning that this product is a an optionnal or + accessory product of the reference_product). + Args: + reference_product (product.product): The reference product that has + the current product as an option or accessory product. + Returns: + The filtered list of product variants + """ + self.ensure_one() + product_product_attribute_values = self.env['product.product.attribute.value'].search([('product_tmpl_id', '=', self.id)]) + if reference_product: + # append the reference_product if provided + product_product_attribute_values |= reference_product.product_attribute_value_ids + product_variants = self.product_variant_ids + + for product_product_attribute_value in product_product_attribute_values: + # CASE 1: The whole product is excluded when no attribute values are selected in the parent product + # returns empty recordset of product.product if so. What is checked is: + # If the product_attribute value doesn't belong to self (i.e. belongs to the reference product) + # and self is the excluded product template on the exclusion lines + # and the exclusions is on the product without specified product attribute values (i.e. the whole product is excluded) + if product_product_attribute_value.product_tmpl_id != self \ + and self in product_product_attribute_value.exclude_for.mapped('product_tmpl_id') \ + and not product_product_attribute_value.exclude_for.filtered( + lambda excluded_product_attribute_value: excluded_product_attribute_value.product_tmpl_id == self).value_ids: + return self.env['product.product'] + + # CASE 2: Check if some of the product.product.attribute.value of the product are excluded + # for this prodcut. A variant could be excluded: + # - Either by itself (eg: The office chair with iron legs excludes the color white) + # - Or by the reference product (eg: The customizable desk with iron legs excludes the office chair with aluminium legs) + for excluded in product_product_attribute_value.exclude_for.filtered( + lambda excluded_product_attribute_value: excluded_product_attribute_value.product_tmpl_id == self): + product_variants -= product_variants.filtered( + lambda variant: + # 1/ Check the applicability of the exlusion + # i.e: the restiction comes from the parent + # OR the restriction is on a product_attribute_value that this variant has, eg: + # if the office chair with iron legs excludes the color white, we must check + # that this variant has iron legs to check the exclusion + (product_product_attribute_value.product_tmpl_id != self or product_product_attribute_value in variant.product_attribute_value_ids) and + # 2/ Check the variant has one of the exluded attribute values + any(attribute_value in excluded.value_ids for attribute_value in variant.product_attribute_value_ids)) + + return product_variants + @api.model def get_empty_list_help(self, help): self = self.with_context( diff --git a/addons/product/models/res_config_settings.py b/addons/product/models/res_config_settings.py index 67988dd6fe2..0750136085e 100644 --- a/addons/product/models/res_config_settings.py +++ b/addons/product/models/res_config_settings.py @@ -13,7 +13,7 @@ class ResConfigSettings(models.TransientModel): " * Checked : Product are visible for every company, even if a company is defined on the partner.\n" " * Unchecked : Each company can see only its product (product where company is defined). Product not related to a company are visible for all companies.") group_uom = fields.Boolean("Units of Measure", implied_group='uom.group_uom') - group_product_variant = fields.Boolean("Attributes and Variants", implied_group='product.group_product_variant') + group_product_variant = fields.Boolean("Variants and Options", implied_group='product.group_product_variant') group_stock_packaging = fields.Boolean('Product Packagings', implied_group='product.group_stock_packaging') group_sale_pricelist = fields.Boolean("Use pricelists to adapt your price per customers", diff --git a/addons/product/security/ir.model.access.csv b/addons/product/security/ir.model.access.csv index 8518bb1fe64..4c76d40b2f6 100644 --- a/addons/product/security/ir.model.access.csv +++ b/addons/product/security/ir.model.access.csv @@ -10,7 +10,8 @@ access_product_product_employee,product.product employee,model_product_product,b access_product_price_history_employee,prices.history employee,model_product_price_history,base.group_user,1,0,0,0 access_product_attribute,product.attribute,model_product_attribute,base.group_user,1,0,0,0 access_product_attribute_value,product.attribute value,model_product_attribute_value,base.group_user,1,0,0,0 -access_product_attribute_price,product.attribute price,model_product_attribute_price,base.group_user,1,0,0,0 +access_product_product_attribute,product.product.attribute value,model_product_product_attribute_value,base.group_user,1,0,0,0 +access_product_attribute_filter_line,product.attribute filter line,model_product_attribute_filter_line,base.group_user,1,0,0,0 access_product_attribute_line,product.attribute line,model_product_attribute_line,base.group_user,1,0,0,0 access_product_category_manager,product.category.manager,model_product_category,base.group_system,1,1,1,1 access_product_template_manager,product.template.manager,model_product_template,base.group_system,1,1,1,1 @@ -22,5 +23,6 @@ access_product_product_manager,product.product manager,model_product_product,bas access_product_price_history_manager,prices.history manager,model_product_price_history,base.group_system,1,1,1,1 access_product_attribute_manager,product.attribute manager,model_product_attribute,base.group_system,1,1,1,1 access_product_attribute_value_manager,product.attribute value manager,model_product_attribute_value,base.group_system,1,1,1,1 -access_product_attribute_price_manager,product.attribute price manager,model_product_attribute_price,base.group_system,1,1,1,1 +access_product_product_attribute_manager,product.product.attribute value manager,model_product_product_attribute_value,base.group_system,1,1,1,1 +access_product_attribute_filter_line_manager,product.attribute filter line manager,model_product_attribute_filter_line,base.group_system,1,1,1,1 access_product_attribute_line_manager,product.attribute line manager,model_product_attribute_line,base.group_system,1,1,1,1 diff --git a/addons/product/tests/__init__.py b/addons/product/tests/__init__.py index 96caad34928..2dfb724036d 100644 --- a/addons/product/tests/__init__.py +++ b/addons/product/tests/__init__.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import test_variants, test_pricelist, test_product_pricelist, test_seller +from . import test_variants +from . import test_pricelist +from . import test_product_pricelist +from . import test_seller +from . import test_product_attribute_value_config diff --git a/addons/product/tests/test_product_attribute_value_config.py b/addons/product/tests/test_product_attribute_value_config.py new file mode 100644 index 00000000000..8a290aa9c02 --- /dev/null +++ b/addons/product/tests/test_product_attribute_value_config.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests.common import TransactionCase + + +class TestProductAttributeValueConfig(TransactionCase): + def setUp(self): + super(TestProductAttributeValueConfig, self).setUp() + + self.computer = self.env['product.template'].create({ + 'name': 'Super Computer' + }) + + self._add_ssd_attribute() + self._add_ram_attribute() + self._add_hdd_attribute() + + self.computer.create_variant_ids() + + self.computer_case = self.env['product.template'].create({ + 'name': 'Super Computer Case' + }) + + self._add_size_attribute() + + self.computer_case.create_variant_ids() + + def test_product_product_attribute_values_creation(self): + self.assertEqual(len(self.computer_ssd_attribute_lines.product_value_ids), 2, 'Product attribute values (ssd) were not automatically created') + self.assertEqual(len(self.computer_ram_attribute_lines.product_value_ids), 3, 'Product attribute values (ram) were not automatically created') + self.assertEqual(len(self.computer_hdd_attribute_lines.product_value_ids), 3, 'Product attribute values (hdd) were not automatically created') + self.assertEqual(len(self.computer_case_size_attribute_lines.product_value_ids), 3, 'Product attribute values (size) were not automatically created') + + def test_product_filtered_exclude_for(self): + """ + Super Computer has 18 variants total (2 ssd * 3 ram * 3 hdd) + RAM 16 excudes HDD 1, that matches 2 variants: + - SSD 256 RAM 16 HDD 1 + - SSD 512 RAM 16 HDD 1 + + => There has to be 16 variants left when filtered + """ + self._add_ram_exclude_for() + self.assertEqual(len(self.computer.get_filtered_variants()), 16) + self.assertFalse(self._get_variant_for_attribute_values(self.computer, [self.ssd_256, self.ram_16, self.hdd_1])) + self.assertFalse(self._get_variant_for_attribute_values(self.computer, [self.ssd_512, self.ram_16, self.hdd_1])) + + def test_children_product_filtered_exclude_for(self): + """ + Super Computer Case has 3 variants total (3 size) + Reference product Computer with HDD 4 excludes Size M + The following variant will be excluded: + - Size M + + => There has to be 2 variants left when filtered + """ + + self._add_hdd_excludes_computer_case() + hdd_4_variant = self.computer.product_variant_ids.filtered( + lambda variant: self.hdd_4 in variant.product_attribute_value_ids.mapped('product_attribute_value_id'))[0] + self.assertEqual(len(self.computer_case.get_filtered_variants(hdd_4_variant)), 2) + self.assertFalse(self._get_variant_for_attribute_values(self.computer_case, [self.size_m], hdd_4_variant)) + + def _add_ssd_attribute(self): + self.ssd_attribute = self.env['product.attribute'].create({'name': 'Memory'}) + self.ssd_256 = self.env['product.attribute.value'].create({ + 'name': '256 GB', + 'attribute_id': self.ssd_attribute.id + }) + self.ssd_512 = self.env['product.attribute.value'].create({ + 'name': '512 GB', + 'attribute_id': self.ssd_attribute.id + }) + self.computer_ssd_attribute_lines = self.env['product.attribute.line'].create({ + 'product_tmpl_id': self.computer.id, + 'attribute_id': self.ssd_attribute.id, + 'value_ids': [(6, 0, [self.ssd_256.id, self.ssd_512.id])] + }) + + def _add_ram_attribute(self): + self.ram_attribute = self.env['product.attribute'].create({'name': 'RAM'}) + self.ram_8 = self.env['product.attribute.value'].create({ + 'name': '8 GB', + 'attribute_id': self.ram_attribute.id + }) + self.ram_16 = self.env['product.attribute.value'].create({ + 'name': '16 GB', + 'attribute_id': self.ram_attribute.id + }) + self.ram_32 = self.env['product.attribute.value'].create({ + 'name': '32 GB', + 'attribute_id': self.ram_attribute.id + }) + self.computer_ram_attribute_lines = self.env['product.attribute.line'].create({ + 'product_tmpl_id': self.computer.id, + 'attribute_id': self.ram_attribute.id, + 'value_ids': [(6, 0, [self.ram_8.id, self.ram_16.id, self.ram_32.id])] + }) + + def _add_hdd_attribute(self): + self.hdd_attribute = self.env['product.attribute'].create({'name': 'HDD'}) + self.hdd_1 = self.env['product.attribute.value'].create({ + 'name': '1 To', + 'attribute_id': self.hdd_attribute.id + }) + self.hdd_2 = self.env['product.attribute.value'].create({ + 'name': '2 To', + 'attribute_id': self.hdd_attribute.id + }) + self.hdd_4 = self.env['product.attribute.value'].create({ + 'name': '4 To', + 'attribute_id': self.hdd_attribute.id + }) + self.computer_hdd_attribute_lines = self.env['product.attribute.line'].create({ + 'product_tmpl_id': self.computer.id, + 'attribute_id': self.hdd_attribute.id, + 'value_ids': [(6, 0, [self.hdd_1.id, self.hdd_2.id, self.hdd_4.id])] + }) + + def _add_ram_exclude_for(self): + self.ram_16_excludes_hdd_1 = self.env['product.attribute.filter.line'].create({ + 'product_tmpl_id': self.computer.id, + 'value_ids': [(6, 0, [self._get_product_value_id(self.computer_hdd_attribute_lines, self.hdd_1).id])] + }) + self._get_product_value_id(self.computer_ram_attribute_lines, self.ram_16).update({ + 'exclude_for': [(6, 0, [self.ram_16_excludes_hdd_1.id])] + }) + + def _add_size_attribute(self): + self.size_attribute = self.env['product.attribute'].create({'name': 'Size'}) + self.size_m = self.env['product.attribute.value'].create({ + 'name': 'M', + 'attribute_id': self.size_attribute.id + }) + self.size_l = self.env['product.attribute.value'].create({ + 'name': 'L', + 'attribute_id': self.size_attribute.id + }) + self.size_xl = self.env['product.attribute.value'].create({ + 'name': 'XL', + 'attribute_id': self.size_attribute.id + }) + self.computer_case_size_attribute_lines = self.env['product.attribute.line'].create({ + 'product_tmpl_id': self.computer_case.id, + 'attribute_id': self.size_attribute.id, + 'value_ids': [(6, 0, [self.size_m.id, self.size_l.id, self.size_xl.id])] + }) + + def _add_hdd_excludes_computer_case(self): + self.hdd_4_excludes_computer_case_m = self.env['product.attribute.filter.line'].create({ + 'product_tmpl_id': self.computer_case.id, + 'value_ids': [(6, 0, [self._get_product_value_id(self.computer_case_size_attribute_lines, self.size_m).id])] + }) + self._get_product_value_id(self.computer_hdd_attribute_lines, self.hdd_4).update({ + 'exclude_for': [(6, 0, [self.hdd_4_excludes_computer_case_m.id])] + }) + + def _get_product_value_id(self, product_attribute_lines, product_attribute_value): + return product_attribute_lines.product_value_ids.filtered( + lambda product_value_id: product_value_id.product_attribute_value_id == product_attribute_value)[0] + + def _get_variant_for_attribute_values(self, product, attribute_values, reference_product=None): + return product.get_filtered_variants(reference_product).filtered( + lambda variant: + all(attribute_value in variant.attribute_value_ids for attribute_value in attribute_values)) diff --git a/addons/product/views/product_attribute_views.xml b/addons/product/views/product_attribute_views.xml index 0cc4c019f13..e3f9df657c4 100644 --- a/addons/product/views/product_attribute_views.xml +++ b/addons/product/views/product_attribute_views.xml @@ -1,96 +1,120 @@ - - product.attribute.value.view.tree - product.attribute.value + + product.product.attribute.value.view.tree + product.product.attribute.value + tree - - + - + + + + product.product.attribute.value.view.form. + product.product.attribute.value + form + +
+ + + + + + + + + + +
+
+
+ - Variant Values + Product Variant Values + product.product.attribute.value + tree,form + [('product_tmpl_id', '=', active_id), ('attribute_id.create_variant', '=', True)] + + { + 'default_product_tmpl_id': active_id + } + + + + product.attribute.tree + product.attribute + + + + + + + + + + + product.attribute.form + product.attribute + +
+ + + + + +
+ +
+
+ + + Attributes + ir.actions.act_window + product.attribute + tree + form + + + + product.attribute.value.tree + product.attribute.value + + + + + + + + + + + Attribute Values ir.actions.act_window product.attribute.value tree form - - [('product_ids.product_tmpl_id', '=', active_id)] - {'default_product_tmpl_id': active_id} - - product.attribute.tree - product.attribute - - - - - - - - - - - product.attribute.form - product.attribute - -
- - - - - -
- -
-
- - - Attributes - ir.actions.act_window - product.attribute - tree - form - - - - product.attribute.value.tree - product.attribute.value - - - - - - - - - - - Attribute Values - ir.actions.act_window - product.attribute.value - tree - form - - product.attribute.line.form product.attribute.line diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml index 48e2714d99c..afeab2387f8 100644 --- a/addons/product/views/product_views.xml +++ b/addons/product/views/product_views.xml @@ -9,7 +9,7 @@
-