diff --git a/addons/product/models/product_attribute.py b/addons/product/models/product_attribute.py index c9d857c34b3..c0d32f94469 100644 --- a/addons/product/models/product_attribute.py +++ b/addons/product/models/product_attribute.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models, _ @@ -12,14 +11,13 @@ class ProductAttribute(models.Model): # `_sort_key_attribute_value` in `product.template` _order = 'sequence, id' - name = fields.Char('Attribute', required=True, translate=True) - value_ids = fields.One2many('product.attribute.value', 'attribute_id', 'Values', copy=True) - sequence = fields.Integer('Sequence', help="Determine the display order", index=True) - attribute_line_ids = fields.One2many('product.template.attribute.line', 'attribute_id', 'Lines') - create_variant = fields.Selection([ - ('always', 'Instantly'), - ('dynamic', 'Dynamically'), - ('no_variant', 'Never (option)')], + name = fields.Char(string="Attribute", required=True, translate=True) + create_variant = fields.Selection( + selection=[ + ('always', 'Instantly'), + ('dynamic', 'Dynamically'), + ('no_variant', 'Never (option)'), + ], default='always', string="Variants Creation Mode", help="""- Instantly: All possible variants are created as soon as the attribute and its values are added to a product. @@ -27,13 +25,33 @@ class ProductAttribute(models.Model): - Never: Variants are never created for the attribute. Note: the variants creation mode cannot be changed once the attribute is used on at least one product.""", required=True) + display_type = fields.Selection( + selection=[ + ('radio', 'Radio'), + ('pills', 'Pills'), + ('select', 'Select'), + ('color', 'Color'), + ], + default='radio', + required=True, + help="The display type used in the Product Configurator.") + sequence = fields.Integer(string="Sequence", help="Determine the display order", index=True) + + value_ids = fields.One2many( + comodel_name='product.attribute.value', + inverse_name='attribute_id', + string="Values", copy=True) + + attribute_line_ids = fields.One2many( + comodel_name='product.template.attribute.line', + inverse_name='attribute_id', + string="Lines") + product_tmpl_ids = fields.Many2many( + comodel_name='product.template', + string="Related Products", + compute='_compute_products', + store=True) number_related_products = fields.Integer(compute='_compute_number_related_products') - product_tmpl_ids = fields.Many2many('product.template', string="Related Products", compute='_compute_products', store=True) - display_type = fields.Selection([ - ('radio', 'Radio'), - ('pills', 'Pills'), - ('select', 'Select'), - ('color', 'Color')], default='radio', required=True, help="The display type used in the Product Configurator.") @api.depends('product_tmpl_ids') def _compute_number_related_products(self): @@ -59,12 +77,14 @@ class ProductAttribute(models.Model): if 'create_variant' in vals: for pa in self: if vals['create_variant'] != pa.create_variant and pa.number_related_products: - raise UserError( - _("You cannot change the Variants Creation Mode of the attribute %s because it is used on the following products:\n%s") % - (pa.display_name, ", ".join(pa.product_tmpl_ids.mapped('display_name'))) - ) + raise UserError(_( + "You cannot change the Variants Creation Mode of the attribute %(attribute)s" + " because it is used on the following products:\n%(products)s", + attribute=pa.display_name, + products=", ".join(pa.product_tmpl_ids.mapped('display_name')), + )) invalidate = 'sequence' in vals and any(record.sequence != vals['sequence'] for record in self) - res = super(ProductAttribute, self).write(vals) + res = super().write(vals) if invalidate: # prefetched o2m have to be resequenced # (eg. product.template: attribute_line_ids) @@ -76,10 +96,12 @@ class ProductAttribute(models.Model): def _unlink_except_used_on_product(self): for pa in self: if pa.number_related_products: - raise UserError( - _("You cannot delete the attribute %s because it is used on the following products:\n%s") % - (pa.display_name, ", ".join(pa.product_tmpl_ids.mapped('display_name'))) - ) + raise UserError(_( + "You cannot delete the attribute %(attribute)s because it is used on the" + " following products:\n%(products)s", + attribute=pa.display_name, + products=", ".join(pa.product_tmpl_ids.mapped('display_name')), + )) def action_open_related_products(self): return { diff --git a/addons/product/models/product_attribute_custom_value.py b/addons/product/models/product_attribute_custom_value.py index 1dd7ad1b5ff..4a8dfab820c 100644 --- a/addons/product/models/product_attribute_custom_value.py +++ b/addons/product/models/product_attribute_custom_value.py @@ -1,17 +1,20 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. - from odoo import api, fields, models class ProductAttributeCustomValue(models.Model): - _name = "product.attribute.custom.value" - _description = 'Product Attribute Custom Value' + _name = 'product.attribute.custom.value' + _description = "Product Attribute Custom Value" _order = 'custom_product_template_attribute_value_id, id' - name = fields.Char("Name", compute='_compute_name') - custom_product_template_attribute_value_id = fields.Many2one('product.template.attribute.value', string="Attribute Value", required=True, ondelete='restrict') - custom_value = fields.Char("Custom Value") + name = fields.Char(string="Name", compute='_compute_name') + custom_product_template_attribute_value_id = fields.Many2one( + comodel_name='product.template.attribute.value', + string="Attribute Value", + required=True, + ondelete='restrict') + custom_value = fields.Char(string="Custom Value") @api.depends('custom_product_template_attribute_value_id.name', 'custom_value') def _compute_name(self): diff --git a/addons/product/models/product_attribute_value.py b/addons/product/models/product_attribute_value.py index 704c334bc1e..e0b41f7ea97 100644 --- a/addons/product/models/product_attribute_value.py +++ b/addons/product/models/product_attribute_value.py @@ -7,33 +7,47 @@ from odoo.exceptions import UserError class ProductAttributeValue(models.Model): - _name = "product.attribute.value" + _name = 'product.attribute.value' # if you change this _order, keep it in sync with the method # `_sort_key_variant` in `product.template' _order = 'attribute_id, sequence, id' - _description = 'Attribute Value' + _description = "Attribute Value" def _get_default_color(self): return randint(1, 11) - name = fields.Char(string='Value', required=True, translate=True) - sequence = fields.Integer(string='Sequence', help="Determine the display order", index=True) - attribute_id = fields.Many2one('product.attribute', string="Attribute", ondelete='cascade', required=True, index=True, - help="The attribute cannot be changed once the value is used on at least one product.") + name = fields.Char(string="Value", required=True, translate=True) + sequence = fields.Integer(string="Sequence", help="Determine the display order", index=True) + attribute_id = fields.Many2one( + comodel_name='product.attribute', + string="Attribute", + help="The attribute cannot be changed once the value is used on at least one product.", + ondelete='cascade', + required=True, + index=True) - pav_attribute_line_ids = fields.Many2many('product.template.attribute.line', string="Lines", - relation='product_attribute_value_product_template_attribute_line_rel', copy=False) - is_used_on_products = fields.Boolean('Used on Products', compute='_compute_is_used_on_products') + pav_attribute_line_ids = fields.Many2many( + comodel_name='product.template.attribute.line', + relation='product_attribute_value_product_template_attribute_line_rel', + string="Lines", + copy=False) + is_used_on_products = fields.Boolean( + string="Used on Products", compute='_compute_is_used_on_products') - is_custom = fields.Boolean('Is custom value', help="Allow users to input custom values for this attribute value") + is_custom = fields.Boolean( + string="Is custom value", + help="Allow users to input custom values for this attribute value") html_color = fields.Char( - string='Color', - help="Here you can set a specific HTML color index (e.g. #ff0000) to display the color if the attribute type is 'Color'.") - display_type = fields.Selection(related='attribute_id.display_type', readonly=True) - color = fields.Integer('Color Index', default=_get_default_color) + string="Color", + help="Here you can set a specific HTML color index (e.g. #ff0000)" + " to display the color if the attribute type is 'Color'.") + display_type = fields.Selection(related='attribute_id.display_type') + color = fields.Integer(string="Color Index", default=_get_default_color) _sql_constraints = [ - ('value_company_uniq', 'unique (name, attribute_id)', "You cannot create two values with the same name for the same attribute.") + ('value_company_uniq', + 'unique (name, attribute_id)', + "You cannot create two values with the same name for the same attribute.") ] @api.depends('pav_attribute_line_ids') @@ -50,21 +64,23 @@ class ProductAttributeValue(models.Model): the attribute is already on each line so there is no need to repeat it on every value. """ - if not self._context.get('show_attribute', True): - return super(ProductAttributeValue, self).name_get() + if not self.env.context.get('show_attribute', True): + return super().name_get() return [(value.id, "%s: %s" % (value.attribute_id.name, value.name)) for value in self] def write(self, values): if 'attribute_id' in values: for pav in self: if pav.attribute_id.id != values['attribute_id'] and pav.is_used_on_products: - raise UserError( - _("You cannot change the attribute of the value %s because it is used on the following products:%s") % - (pav.display_name, ", ".join(pav.pav_attribute_line_ids.product_tmpl_id.mapped('display_name'))) - ) + raise UserError(_( + "You cannot change the attribute of the value %(value)s because it is used" + " on the following products: %(products)s", + value=pav.display_name, + products=", ".join(pav.pav_attribute_line_ids.product_tmpl_id.mapped('display_name')), + )) invalidate = 'sequence' in values and any(record.sequence != values['sequence'] for record in self) - res = super(ProductAttributeValue, self).write(values) + res = super().write(values) if invalidate: # prefetched o2m have to be resequenced # (eg. product.template.attribute.line: value_ids) @@ -76,10 +92,11 @@ class ProductAttributeValue(models.Model): def _unlink_except_used_on_product(self): for pav in self: if pav.is_used_on_products: - raise UserError( - _("You cannot delete the value %s because it is used on the following products:\n%s") % - (pav.display_name, ", ".join(pav.pav_attribute_line_ids.product_tmpl_id.mapped('display_name'))) - ) + raise UserError(_( + "You cannot delete the value %(value)s because it is used on the following products:\n%(products)s", + value=pav.display_name, + products=", ".join(pav.pav_attribute_line_ids.product_tmpl_id.mapped('display_name')), + )) def _without_no_variant_attributes(self): return self.filtered(lambda pav: pav.attribute_id.create_variant != 'no_variant') diff --git a/addons/product/models/product_template_attribute_exclusion.py b/addons/product/models/product_template_attribute_exclusion.py index da28dd100db..1878e94b19f 100644 --- a/addons/product/models/product_template_attribute_exclusion.py +++ b/addons/product/models/product_template_attribute_exclusion.py @@ -4,14 +4,23 @@ from odoo import fields, models class ProductTemplateAttributeExclusion(models.Model): - _name = "product.template.attribute.exclusion" - _description = 'Product Template Attribute Exclusion' + _name = 'product.template.attribute.exclusion' + _description = "Product Template Attribute Exclusion" _order = 'product_tmpl_id, id' product_template_attribute_value_id = fields.Many2one( - 'product.template.attribute.value', string="Attribute Value", ondelete='cascade', index=True) + comodel_name='product.template.attribute.value', + string="Attribute Value", + ondelete='cascade', + index=True) product_tmpl_id = fields.Many2one( - 'product.template', string='Product Template', ondelete='cascade', required=True, index=True) + comodel_name='product.template', + string="Product Template", + ondelete='cascade', + required=True, + index=True) value_ids = fields.Many2many( - 'product.template.attribute.value', relation="product_attr_exclusion_value_ids_rel", - string='Attribute Values', domain="[('product_tmpl_id', '=', product_tmpl_id), ('ptav_active', '=', True)]") + comodel_name='product.template.attribute.value', + relation='product_attr_exclusion_value_ids_rel', + string="Attribute Values", + domain="[('product_tmpl_id', '=', product_tmpl_id), ('ptav_active', '=', True)]") diff --git a/addons/product/models/product_template_attribute_line.py b/addons/product/models/product_template_attribute_line.py index eff34ae24f5..adfc88170ad 100644 --- a/addons/product/models/product_template_attribute_line.py +++ b/addons/product/models/product_template_attribute_line.py @@ -2,25 +2,43 @@ from odoo import api, fields, models, tools, _ from odoo.exceptions import UserError, ValidationError +from odoo.fields import Command class ProductTemplateAttributeLine(models.Model): """Attributes available on product.template with their selected values in a m2m. Used as a configuration model to generate the appropriate product.template.attribute.value""" - _name = "product.template.attribute.line" + _name = 'product.template.attribute.line' _rec_name = 'attribute_id' _rec_names_search = ['attribute_id', 'value_ids'] - _description = 'Product Template Attribute Line' + _description = "Product Template Attribute Line" _order = 'attribute_id, id' active = fields.Boolean(default=True) - product_tmpl_id = fields.Many2one('product.template', string="Product Template", ondelete='cascade', required=True, index=True) - attribute_id = fields.Many2one('product.attribute', string="Attribute", ondelete='restrict', required=True, index=True) - value_ids = fields.Many2many('product.attribute.value', string="Values", domain="[('attribute_id', '=', attribute_id)]", - relation='product_attribute_value_product_template_attribute_line_rel', ondelete='restrict') - value_count = fields.Integer(compute='_compute_value_count', store=True, readonly=True) - product_template_value_ids = fields.One2many('product.template.attribute.value', 'attribute_line_id', string="Product Attribute Values") + product_tmpl_id = fields.Many2one( + comodel_name='product.template', + string="Product Template", + ondelete='cascade', + required=True, + index=True) + attribute_id = fields.Many2one( + comodel_name='product.attribute', + string="Attribute", + ondelete='restrict', + required=True, + index=True) + value_ids = fields.Many2many( + comodel_name='product.attribute.value', + relation='product_attribute_value_product_template_attribute_line_rel', + string="Values", + domain="[('attribute_id', '=', attribute_id)]", + ondelete='restrict') + value_count = fields.Integer(compute='_compute_value_count', store=True) + product_template_value_ids = fields.One2many( + comodel_name='product.template.attribute.value', + inverse_name='attribute_line_id', + string="Product Attribute Values") @api.depends('value_ids') def _compute_value_count(self): @@ -35,16 +53,20 @@ class ProductTemplateAttributeLine(models.Model): def _check_valid_values(self): for ptal in self: if ptal.active and not ptal.value_ids: - raise ValidationError( - _("The attribute %s must have at least one value for the product %s.") % - (ptal.attribute_id.display_name, ptal.product_tmpl_id.display_name) - ) + raise ValidationError(_( + "The attribute %(attribute)s must have at least one value for the product %(product)s.", + attribute=ptal.attribute_id.display_name, + product=ptal.product_tmpl_id.display_name, + )) for pav in ptal.value_ids: if pav.attribute_id != ptal.attribute_id: - raise ValidationError( - _("On the product %s you cannot associate the value %s with the attribute %s because they do not match.") % - (ptal.product_tmpl_id.display_name, pav.display_name, ptal.attribute_id.display_name) - ) + raise ValidationError(_( + "On the product %(product)s you cannot associate the value %(value)s" + " with the attribute %(attribute)s because they do not match.", + product=ptal.product_tmpl_id.display_name, + value=pav.display_name, + attribute=ptal.attribute_id.display_name, + )) return True @api.model_create_multi @@ -79,7 +101,7 @@ class ProductTemplateAttributeLine(models.Model): activated_lines += archived_ptal else: create_values.append(value) - res = activated_lines + super(ProductTemplateAttributeLine, self).create(create_values) + res = activated_lines + super().create(create_values) res._update_product_template_attribute_values() return res @@ -93,23 +115,29 @@ class ProductTemplateAttributeLine(models.Model): if 'product_tmpl_id' in values: for ptal in self: if ptal.product_tmpl_id.id != values['product_tmpl_id']: - raise UserError( - _("You cannot move the attribute %s from the product %s to the product %s.") % - (ptal.attribute_id.display_name, ptal.product_tmpl_id.display_name, values['product_tmpl_id']) - ) + raise UserError(_( + "You cannot move the attribute %(attribute)s from the product" + " %(product_src)s to the product %(product_dest)s.", + attribute=ptal.attribute_id.display_name, + product_src=ptal.product_tmpl_id.display_name, + product_dest=values['product_tmpl_id'], + )) if 'attribute_id' in values: for ptal in self: if ptal.attribute_id.id != values['attribute_id']: - raise UserError( - _("On the product %s you cannot transform the attribute %s into the attribute %s.") % - (ptal.product_tmpl_id.display_name, ptal.attribute_id.display_name, values['attribute_id']) - ) + raise UserError(_( + "On the product %(product)s you cannot transform the attribute" + " %(attribute_src)s into the attribute %(attribute_dest)s.", + product=ptal.product_tmpl_id.display_name, + attribute_src=ptal.attribute_id.display_name, + attribute_dest=values['attribute_id'], + )) # Remove all values while archiving to make sure the line is clean if it # is ever activated again. if not values.get('active', True): - values['value_ids'] = [(5, 0, 0)] - res = super(ProductTemplateAttributeLine, self).write(values) + values['value_ids'] = [Command.clear()] + res = super().write(values) if 'active' in values: self.env.flush_all() self.env['product.template'].invalidate_model(['attribute_line_ids']) diff --git a/addons/product/models/product_template_attribute_value.py b/addons/product/models/product_template_attribute_value.py index 9a1ec6df106..eea2dd5bab7 100644 --- a/addons/product/models/product_template_attribute_value.py +++ b/addons/product/models/product_template_attribute_value.py @@ -4,13 +4,14 @@ from random import randint from odoo import api, fields, models, tools, _ from odoo.exceptions import UserError, ValidationError +from odoo.fields import Command class ProductTemplateAttributeValue(models.Model): """Materialized relationship between attribute values and product template generated by the product.template.attribute.line""" - _name = "product.template.attribute.value" + _name = 'product.template.attribute.value' _description = "Product Template Attribute Value" _order = 'attribute_line_id, product_attribute_value_id, id' @@ -19,51 +20,64 @@ class ProductTemplateAttributeValue(models.Model): # Not just `active` because we always want to show the values except in # specific case, as opposed to `active_test`. - ptav_active = fields.Boolean("Active", default=True) - name = fields.Char('Value', related="product_attribute_value_id.name") + ptav_active = fields.Boolean(string="Active", default=True) + name = fields.Char(string="Value", related="product_attribute_value_id.name") # defining fields: the product template attribute line and the product attribute value product_attribute_value_id = fields.Many2one( - 'product.attribute.value', string='Attribute Value', + comodel_name='product.attribute.value', + string="Attribute Value", + required=True, ondelete='cascade', index=True) + attribute_line_id = fields.Many2one( + comodel_name='product.template.attribute.line', required=True, ondelete='cascade', index=True) - attribute_line_id = fields.Many2one('product.template.attribute.line', required=True, ondelete='cascade', index=True) # configuration fields: the price_extra and the exclusion rules price_extra = fields.Float( string="Value Price Extra", default=0.0, digits='Product Price', - help="Extra price for the variant with this attribute value on sale price. eg. 200 price extra, 1000 + 200 = 1200.") + help="Extra price for the variant with this attribute value on sale price." + " eg. 200 price extra, 1000 + 200 = 1200.") currency_id = fields.Many2one(related='attribute_line_id.product_tmpl_id.currency_id') exclude_for = fields.One2many( - 'product.template.attribute.exclusion', - 'product_template_attribute_value_id', + comodel_name='product.template.attribute.exclusion', + inverse_name='product_template_attribute_value_id', string="Exclude for", help="Make this attribute value not compatible with " "other values of the product or some attribute values of optional and accessory products.") # related fields: product template and product attribute - product_tmpl_id = fields.Many2one('product.template', string="Product Template", related='attribute_line_id.product_tmpl_id', store=True, index=True) - attribute_id = fields.Many2one('product.attribute', string="Attribute", related='attribute_line_id.attribute_id', store=True, index=True) - ptav_product_variant_ids = fields.Many2many('product.product', relation='product_variant_combination', string="Related Variants", readonly=True) + product_tmpl_id = fields.Many2one( + related='attribute_line_id.product_tmpl_id', store=True, index=True) + attribute_id = fields.Many2one( + related='attribute_line_id.attribute_id', store=True, index=True) + ptav_product_variant_ids = fields.Many2many( + comodel_name='product.product', relation='product_variant_combination', + string="Related Variants", readonly=True) - html_color = fields.Char('HTML Color Index', related="product_attribute_value_id.html_color") - is_custom = fields.Boolean('Is custom value', related="product_attribute_value_id.is_custom") - display_type = fields.Selection(related='product_attribute_value_id.display_type', readonly=True) - color = fields.Integer('Color', default=_get_default_color) + html_color = fields.Char(string="HTML Color Index", related='product_attribute_value_id.html_color') + is_custom = fields.Boolean(related='product_attribute_value_id.is_custom') + display_type = fields.Selection(related='product_attribute_value_id.display_type') + color = fields.Integer(string="Color", default=_get_default_color) _sql_constraints = [ - ('attribute_value_unique', 'unique(attribute_line_id, product_attribute_value_id)', "Each value should be defined only once per attribute per product."), + ('attribute_value_unique', + 'unique(attribute_line_id, product_attribute_value_id)', + "Each value should be defined only once per attribute per product."), ] @api.constrains('attribute_line_id', 'product_attribute_value_id') def _check_valid_values(self): for ptav in self: if ptav.product_attribute_value_id not in ptav.attribute_line_id.value_ids: - raise ValidationError( - _("The value %s is not defined for the attribute %s on the product %s.") % - (ptav.product_attribute_value_id.display_name, ptav.attribute_id.display_name, ptav.product_tmpl_id.display_name) - ) + raise ValidationError(_( + "The value %(value)s is not defined for the attribute %(attribute)s" + " on the product %(product)s.", + value=ptav.product_attribute_value_id.display_name, + attribute=ptav.attribute_id.display_name, + product=ptav.product_tmpl_id.display_name, + )) @api.model_create_multi def create(self, vals_list): @@ -71,7 +85,7 @@ class ProductTemplateAttributeValue(models.Model): # Force write on this relation from `product.product` to properly # trigger `_compute_combination_indices`. raise UserError(_("You cannot update related variants from the values. Please update related values from the variants.")) - return super(ProductTemplateAttributeValue, self).create(vals_list) + return super().create(vals_list) def write(self, values): if 'ptav_product_variant_ids' in values: @@ -83,16 +97,18 @@ class ProductTemplateAttributeValue(models.Model): if pav_in_values or product_in_values: for ptav in self: if pav_in_values and ptav.product_attribute_value_id.id != values['product_attribute_value_id']: - raise UserError( - _("You cannot change the value of the value %s set on product %s.") % - (ptav.display_name, ptav.product_tmpl_id.display_name) - ) + raise UserError(_( + "You cannot change the value of the value %(value)s set on product %(product)s.", + value=ptav.display_name, + product=ptav.product_tmpl_id.display_name, + )) if product_in_values and ptav.product_tmpl_id.id != values['product_tmpl_id']: - raise UserError( - _("You cannot change the product of the value %s set on product %s.") % - (ptav.display_name, ptav.product_tmpl_id.display_name) - ) - res = super(ProductTemplateAttributeValue, self).write(values) + raise UserError(_( + "You cannot change the product of the value %(value)s set on product %(product)s.", + value=ptav.display_name, + product=ptav.product_tmpl_id.display_name, + )) + res = super().write(values) if 'exclude_for' in values: self.product_tmpl_id._create_variant_ids() return res @@ -112,7 +128,9 @@ class ProductTemplateAttributeValue(models.Model): # value (counting also the values that are archived). single_values = self.filtered(lambda ptav: len(ptav.attribute_line_id.product_template_value_ids) == 1) for ptav in single_values: - ptav.ptav_product_variant_ids.write({'product_template_attribute_value_ids': [(3, ptav.id, 0)]}) + ptav.ptav_product_variant_ids.write({ + 'product_template_attribute_value_ids': [Command.unlink(ptav.id)], + }) # Try to remove the variants before deleting to potentially remove some # blocking references. self.ptav_product_variant_ids._unlink_or_archive()