summaryrefslogtreecommitdiff
path: root/addons/product/models/product_attribute.py
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/product/models/product_attribute.py
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/product/models/product_attribute.py')
-rw-r--r--addons/product/models/product_attribute.py573
1 files changed, 573 insertions, 0 deletions
diff --git a/addons/product/models/product_attribute.py b/addons/product/models/product_attribute.py
new file mode 100644
index 00000000..8c252c22
--- /dev/null
+++ b/addons/product/models/product_attribute.py
@@ -0,0 +1,573 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models, tools, _
+from odoo.exceptions import UserError, ValidationError
+from odoo.osv import expression
+
+
+class ProductAttribute(models.Model):
+ _name = "product.attribute"
+ _description = "Product Attribute"
+ # if you change this _order, keep it in sync with the method
+ # `_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')],
+ 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.
+ - Dynamically: Each variant is created only when its corresponding attributes and values are added to a sales order.
+ - 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)
+ is_used_on_products = fields.Boolean('Used on Products', compute='_compute_is_used_on_products')
+ product_tmpl_ids = fields.Many2many('product.template', string="Related Products", compute='_compute_products', store=True)
+ display_type = fields.Selection([
+ ('radio', 'Radio'),
+ ('select', 'Select'),
+ ('color', 'Color')], default='radio', required=True, help="The display type used in the Product Configurator.")
+
+ @api.depends('product_tmpl_ids')
+ def _compute_is_used_on_products(self):
+ for pa in self:
+ pa.is_used_on_products = bool(pa.product_tmpl_ids)
+
+ @api.depends('attribute_line_ids.active', 'attribute_line_ids.product_tmpl_id')
+ def _compute_products(self):
+ for pa in self:
+ pa.product_tmpl_ids = pa.attribute_line_ids.product_tmpl_id
+
+ def _without_no_variant_attributes(self):
+ return self.filtered(lambda pa: pa.create_variant != 'no_variant')
+
+ def write(self, vals):
+ """Override to make sure attribute type can't be changed if it's used on
+ a product template.
+
+ This is important to prevent because changing the type would make
+ existing combinations invalid without recomputing them, and recomputing
+ them might take too long and we don't want to change products without
+ the user knowing about it."""
+ if 'create_variant' in vals:
+ for pa in self:
+ if vals['create_variant'] != pa.create_variant and pa.is_used_on_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')))
+ )
+ invalidate_cache = 'sequence' in vals and any(record.sequence != vals['sequence'] for record in self)
+ res = super(ProductAttribute, self).write(vals)
+ if invalidate_cache:
+ # prefetched o2m have to be resequenced
+ # (eg. product.template: attribute_line_ids)
+ self.flush()
+ self.invalidate_cache()
+ return res
+
+ def unlink(self):
+ for pa in self:
+ if pa.is_used_on_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')))
+ )
+ return super(ProductAttribute, self).unlink()
+
+
+class ProductAttributeValue(models.Model):
+ _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'
+
+ 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.")
+
+ 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')
+
+ is_custom = fields.Boolean('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)
+
+ _sql_constraints = [
+ ('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')
+ def _compute_is_used_on_products(self):
+ for pav in self:
+ pav.is_used_on_products = bool(pav.pav_attribute_line_ids)
+
+ def name_get(self):
+ """Override because in general the name of the value is confusing if it
+ is displayed without the name of the corresponding attribute.
+ Eg. on product list & kanban views, on BOM form view
+
+ However during variant set up (on the product template form) the name of
+ 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()
+ 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')))
+ )
+
+ invalidate_cache = 'sequence' in values and any(record.sequence != values['sequence'] for record in self)
+ res = super(ProductAttributeValue, self).write(values)
+ if invalidate_cache:
+ # prefetched o2m have to be resequenced
+ # (eg. product.template.attribute.line: value_ids)
+ self.flush()
+ self.invalidate_cache()
+ return res
+
+ def unlink(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')))
+ )
+ return super(ProductAttributeValue, self).unlink()
+
+ def _without_no_variant_attributes(self):
+ return self.filtered(lambda pav: pav.attribute_id.create_variant != 'no_variant')
+
+
+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"
+ _rec_name = 'attribute_id'
+ _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')
+ product_template_value_ids = fields.One2many('product.template.attribute.value', 'attribute_line_id', string="Product Attribute Values")
+
+ @api.onchange('attribute_id')
+ def _onchange_attribute_id(self):
+ self.value_ids = self.value_ids.filtered(lambda pav: pav.attribute_id == self.attribute_id)
+
+ @api.constrains('active', 'value_ids', 'attribute_id')
+ 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)
+ )
+ 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)
+ )
+ return True
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ """Override to:
+ - Activate archived lines having the same configuration (if they exist)
+ instead of creating new lines.
+ - Set up related values and related variants.
+
+ Reactivating existing lines allows to re-use existing variants when
+ possible, keeping their configuration and avoiding duplication.
+ """
+ create_values = []
+ activated_lines = self.env['product.template.attribute.line']
+ for value in vals_list:
+ vals = dict(value, active=value.get('active', True))
+ # While not ideal for peformance, this search has to be done at each
+ # step to exclude the lines that might have been activated at a
+ # previous step. Since `vals_list` will likely be a small list in
+ # all use cases, this is an acceptable trade-off.
+ archived_ptal = self.search([
+ ('active', '=', False),
+ ('product_tmpl_id', '=', vals.pop('product_tmpl_id', 0)),
+ ('attribute_id', '=', vals.pop('attribute_id', 0)),
+ ], limit=1)
+ if archived_ptal:
+ # Write given `vals` in addition of `active` to ensure
+ # `value_ids` or other fields passed to `create` are saved too,
+ # but change the context to avoid updating the values and the
+ # variants until all the expected lines are created/updated.
+ archived_ptal.with_context(update_product_template_attribute_values=False).write(vals)
+ activated_lines += archived_ptal
+ else:
+ create_values.append(value)
+ res = activated_lines + super(ProductTemplateAttributeLine, self).create(create_values)
+ res._update_product_template_attribute_values()
+ return res
+
+ def write(self, values):
+ """Override to:
+ - Add constraints to prevent doing changes that are not supported such
+ as modifying the template or the attribute of existing lines.
+ - Clean up related values and related variants when archiving or when
+ updating `value_ids`.
+ """
+ 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'])
+ )
+
+ 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'])
+ )
+ # 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)
+ if 'active' in values:
+ self.flush()
+ self.env['product.template'].invalidate_cache(fnames=['attribute_line_ids'])
+ # If coming from `create`, no need to update the values and the variants
+ # before all lines are created.
+ if self.env.context.get('update_product_template_attribute_values', True):
+ self._update_product_template_attribute_values()
+ return res
+
+ def unlink(self):
+ """Override to:
+ - Archive the line if unlink is not possible.
+ - Clean up related values and related variants.
+
+ Archiving is typically needed when the line has values that can't be
+ deleted because they are referenced elsewhere (on a variant that can't
+ be deleted, on a sales order line, ...).
+ """
+ # Try to remove the values first to remove some potentially blocking
+ # references, which typically works:
+ # - For single value lines because the values are directly removed from
+ # the variants.
+ # - For values that are present on variants that can be deleted.
+ self.product_template_value_ids._only_active().unlink()
+ # Keep a reference to the related templates before the deletion.
+ templates = self.product_tmpl_id
+ # Now delete or archive the lines.
+ ptal_to_archive = self.env['product.template.attribute.line']
+ for ptal in self:
+ try:
+ with self.env.cr.savepoint(), tools.mute_logger('odoo.sql_db'):
+ super(ProductTemplateAttributeLine, ptal).unlink()
+ except Exception:
+ # We catch all kind of exceptions to be sure that the operation
+ # doesn't fail.
+ ptal_to_archive += ptal
+ ptal_to_archive.write({'active': False})
+ # For archived lines `_update_product_template_attribute_values` is
+ # implicitly called during the `write` above, but for products that used
+ # unlinked lines `_create_variant_ids` has to be called manually.
+ (templates - ptal_to_archive.product_tmpl_id)._create_variant_ids()
+ return True
+
+ def _update_product_template_attribute_values(self):
+ """Create or unlink `product.template.attribute.value` for each line in
+ `self` based on `value_ids`.
+
+ The goal is to delete all values that are not in `value_ids`, to
+ activate those in `value_ids` that are currently archived, and to create
+ those in `value_ids` that didn't exist.
+
+ This is a trick for the form view and for performance in general,
+ because we don't want to generate in advance all possible values for all
+ templates, but only those that will be selected.
+ """
+ ProductTemplateAttributeValue = self.env['product.template.attribute.value']
+ ptav_to_create = []
+ ptav_to_unlink = ProductTemplateAttributeValue
+ for ptal in self:
+ ptav_to_activate = ProductTemplateAttributeValue
+ remaining_pav = ptal.value_ids
+ for ptav in ptal.product_template_value_ids:
+ if ptav.product_attribute_value_id not in remaining_pav:
+ # Remove values that existed but don't exist anymore, but
+ # ignore those that are already archived because if they are
+ # archived it means they could not be deleted previously.
+ if ptav.ptav_active:
+ ptav_to_unlink += ptav
+ else:
+ # Activate corresponding values that are currently archived.
+ remaining_pav -= ptav.product_attribute_value_id
+ if not ptav.ptav_active:
+ ptav_to_activate += ptav
+
+ for pav in remaining_pav:
+ # The previous loop searched for archived values that belonged to
+ # the current line, but if the line was deleted and another line
+ # was recreated for the same attribute, we need to expand the
+ # search to those with matching `attribute_id`.
+ # While not ideal for peformance, this search has to be done at
+ # each step to exclude the values that might have been activated
+ # at a previous step. Since `remaining_pav` will likely be a
+ # small list in all use cases, this is an acceptable trade-off.
+ ptav = ProductTemplateAttributeValue.search([
+ ('ptav_active', '=', False),
+ ('product_tmpl_id', '=', ptal.product_tmpl_id.id),
+ ('attribute_id', '=', ptal.attribute_id.id),
+ ('product_attribute_value_id', '=', pav.id),
+ ], limit=1)
+ if ptav:
+ ptav.write({'ptav_active': True, 'attribute_line_id': ptal.id})
+ # If the value was marked for deletion, now keep it.
+ ptav_to_unlink -= ptav
+ else:
+ # create values that didn't exist yet
+ ptav_to_create.append({
+ 'product_attribute_value_id': pav.id,
+ 'attribute_line_id': ptal.id
+ })
+ # Handle active at each step in case a following line might want to
+ # re-use a value that was archived at a previous step.
+ ptav_to_activate.write({'ptav_active': True})
+ ptav_to_unlink.write({'ptav_active': False})
+ ptav_to_unlink.unlink()
+ ProductTemplateAttributeValue.create(ptav_to_create)
+ self.product_tmpl_id._create_variant_ids()
+
+ @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
+ # search on a m2o and one on a m2m, probably this will quickly become
+ # difficult to compute - check if performance optimization is required
+ if name and operator in ('=', 'ilike', '=ilike', 'like', '=like'):
+ args = args or []
+ domain = ['|', ('attribute_id', operator, name), ('value_ids', operator, name)]
+ return self._search(expression.AND([domain, args]), limit=limit, access_rights_uid=name_get_uid)
+ return super(ProductTemplateAttributeLine, self)._name_search(name=name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid)
+
+ def _without_no_variant_attributes(self):
+ return self.filtered(lambda ptal: ptal.attribute_id.create_variant != 'no_variant')
+
+
+class ProductTemplateAttributeValue(models.Model):
+ """Materialized relationship between attribute values
+ and product template generated by the product.template.attribute.line"""
+
+ _name = "product.template.attribute.value"
+ _description = "Product Template Attribute Value"
+ _order = 'attribute_line_id, product_attribute_value_id, id'
+
+ # 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")
+
+ # defining fields: the product template attribute line and the product attribute value
+ product_attribute_value_id = fields.Many2one(
+ 'product.attribute.value', string='Attribute Value',
+ 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.")
+ 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',
+ 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)
+
+ 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)
+
+ _sql_constraints = [
+ ('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)
+ )
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ if any('ptav_product_variant_ids' in v for v in vals_list):
+ # 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)
+
+ def write(self, values):
+ if 'ptav_product_variant_ids' in values:
+ # 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."))
+ pav_in_values = 'product_attribute_value_id' in values
+ product_in_values = 'product_tmpl_id' in values
+ 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)
+ )
+ 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)
+ )
+ return super(ProductTemplateAttributeValue, self).write(values)
+
+ def unlink(self):
+ """Override to:
+ - Clean up the variants that use any of the values in self:
+ - Remove the value from the variant if the value belonged to an
+ attribute line with only one value.
+ - Unlink or archive all related variants.
+ - Archive the value if unlink is not possible.
+
+ Archiving is typically needed when the value is referenced elsewhere
+ (on a variant that can't be deleted, on a sales order line, ...).
+ """
+ # Directly remove the values from the variants for lines that had single
+ # 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)]})
+ # Try to remove the variants before deleting to potentially remove some
+ # blocking references.
+ self.ptav_product_variant_ids._unlink_or_archive()
+ # Now delete or archive the values.
+ ptav_to_archive = self.env['product.template.attribute.value']
+ for ptav in self:
+ try:
+ with self.env.cr.savepoint(), tools.mute_logger('odoo.sql_db'):
+ super(ProductTemplateAttributeValue, ptav).unlink()
+ except Exception:
+ # We catch all kind of exceptions to be sure that the operation
+ # doesn't fail.
+ ptav_to_archive += ptav
+ ptav_to_archive.write({'ptav_active': False})
+ return True
+
+ def name_get(self):
+ """Override because in general the name of the value is confusing if it
+ is displayed without the name of the corresponding attribute.
+ Eg. on exclusion rules form
+ """
+ return [(value.id, "%s: %s" % (value.attribute_id.name, value.name)) for value in self]
+
+ def _only_active(self):
+ return self.filtered(lambda ptav: ptav.ptav_active)
+
+ def _without_no_variant_attributes(self):
+ return self.filtered(lambda ptav: ptav.attribute_id.create_variant != 'no_variant')
+
+ def _ids2str(self):
+ return ','.join([str(i) for i in sorted(self.ids)])
+
+ def _get_combination_name(self):
+ """Exclude values from single value lines or from no_variant attributes."""
+ return ", ".join([ptav.name for ptav in self._without_no_variant_attributes()._filter_single_value_lines()])
+
+ def _filter_single_value_lines(self):
+ """Return `self` with values from single value lines filtered out
+ depending on the active state of all the values in `self`.
+
+ If any value in `self` is archived, archived values are also taken into
+ account when checking for single values.
+ This allows to display the correct name for archived variants.
+
+ If all values in `self` are active, only active values are taken into
+ account when checking for single values.
+ This allows to display the correct name for active combinations.
+ """
+ only_active = all(ptav.ptav_active for ptav in self)
+ return self.filtered(lambda ptav: not ptav._is_from_single_value_line(only_active))
+
+ def _is_from_single_value_line(self, only_active=True):
+ """Return whether `self` is from a single value line, counting also
+ archived values if `only_active` is False.
+ """
+ self.ensure_one()
+ all_values = self.attribute_line_id.product_template_value_ids
+ if only_active:
+ all_values = all_values._only_active()
+ return len(all_values) == 1
+
+
+class ProductTemplateAttributeExclusion(models.Model):
+ _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)
+ product_tmpl_id = fields.Many2one(
+ '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)]")
+
+
+class ProductAttributeCustomValue(models.Model):
+ _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")
+
+ @api.depends('custom_product_template_attribute_value_id.name', 'custom_value')
+ def _compute_name(self):
+ for record in self:
+ name = (record.custom_value or '').strip()
+ if record.custom_product_template_attribute_value_id.display_name:
+ name = "%s: %s" % (record.custom_product_template_attribute_value_id.display_name, name)
+ record.name = name