645 lines
32 KiB
Python
645 lines
32 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
from random import randint
|
||
|
|
||
|
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, default=20)
|
||
|
attribute_line_ids = fields.One2many('product.template.attribute.line', 'attribute_id', 'Lines')
|
||
|
create_variant = fields.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.
|
||
|
- 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)
|
||
|
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):
|
||
|
for pa in self:
|
||
|
pa.number_related_products = len(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.with_context(active_test=False).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.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')))
|
||
|
)
|
||
|
invalidate = 'sequence' in vals and any(record.sequence != vals['sequence'] for record in self)
|
||
|
res = super(ProductAttribute, self).write(vals)
|
||
|
if invalidate:
|
||
|
# prefetched o2m have to be resequenced
|
||
|
# (eg. product.template: attribute_line_ids)
|
||
|
self.env.flush_all()
|
||
|
self.env.invalidate_all()
|
||
|
return res
|
||
|
|
||
|
@api.ondelete(at_uninstall=False)
|
||
|
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')))
|
||
|
)
|
||
|
|
||
|
def action_open_related_products(self):
|
||
|
return {
|
||
|
'type': 'ir.actions.act_window',
|
||
|
'name': _("Related Products"),
|
||
|
'res_model': 'product.template',
|
||
|
'view_mode': 'tree,form',
|
||
|
'domain': [('id', 'in', self.product_tmpl_ids.ids)],
|
||
|
}
|
||
|
|
||
|
|
||
|
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'
|
||
|
|
||
|
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.")
|
||
|
|
||
|
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)
|
||
|
color = fields.Integer('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.")
|
||
|
]
|
||
|
|
||
|
@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 = 'sequence' in values and any(record.sequence != values['sequence'] for record in self)
|
||
|
res = super(ProductAttributeValue, self).write(values)
|
||
|
if invalidate:
|
||
|
# prefetched o2m have to be resequenced
|
||
|
# (eg. product.template.attribute.line: value_ids)
|
||
|
self.env.flush_all()
|
||
|
self.env.invalidate_all()
|
||
|
return res
|
||
|
|
||
|
@api.ondelete(at_uninstall=False)
|
||
|
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\n If the value has been associated to a product in the past, you will "
|
||
|
"not be able to delete it.") %
|
||
|
(pav.display_name, ", ".join(
|
||
|
pav.pav_attribute_line_ids.product_tmpl_id.mapped('display_name')
|
||
|
))
|
||
|
)
|
||
|
linked_products = pav.env['product.template.attribute.value'].search(
|
||
|
[('product_attribute_value_id', '=', pav.id)]
|
||
|
).with_context(active_test=False).ptav_product_variant_ids
|
||
|
unlinkable_products = linked_products._filter_to_unlink()
|
||
|
if linked_products != unlinkable_products:
|
||
|
raise UserError(_(
|
||
|
"You cannot delete value %s because it was used in some products.",
|
||
|
pav.display_name
|
||
|
))
|
||
|
|
||
|
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'
|
||
|
_rec_names_search = ['attribute_id', 'value_ids']
|
||
|
_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")
|
||
|
|
||
|
@api.depends('value_ids')
|
||
|
def _compute_value_count(self):
|
||
|
for record in self:
|
||
|
record.value_count = len(record.value_ids)
|
||
|
|
||
|
@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.env.flush_all()
|
||
|
self.env['product.template'].invalidate_model(['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.action_archive() # only calls write if there are records
|
||
|
# 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})
|
||
|
if ptav_to_unlink:
|
||
|
ptav_to_unlink.unlink()
|
||
|
ProductTemplateAttributeValue.create(ptav_to_create)
|
||
|
self.product_tmpl_id._create_variant_ids()
|
||
|
|
||
|
def _without_no_variant_attributes(self):
|
||
|
return self.filtered(lambda ptal: ptal.attribute_id.create_variant != 'no_variant')
|
||
|
|
||
|
def action_open_attribute_values(self):
|
||
|
return {
|
||
|
'type': 'ir.actions.act_window',
|
||
|
'name': _("Product Variant Values"),
|
||
|
'res_model': 'product.template.attribute.value',
|
||
|
'view_mode': 'tree,form',
|
||
|
'domain': [('id', 'in', self.product_template_value_ids.ids)],
|
||
|
'views': [
|
||
|
(self.env.ref('product.product_template_attribute_value_view_tree').id, 'list'),
|
||
|
(self.env.ref('product.product_template_attribute_value_view_form').id, 'form'),
|
||
|
],
|
||
|
'context': {
|
||
|
'search_default_active': 1,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
|
||
|
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'
|
||
|
|
||
|
def _get_default_color(self):
|
||
|
return randint(1, 11)
|
||
|
|
||
|
# 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)
|
||
|
color = fields.Integer('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."),
|
||
|
]
|
||
|
|
||
|
@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)
|
||
|
)
|
||
|
res = super(ProductTemplateAttributeValue, self).write(values)
|
||
|
if 'exclude_for' in values:
|
||
|
self.product_tmpl_id._create_variant_ids()
|
||
|
return res
|
||
|
|
||
|
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."""
|
||
|
ptavs = self._without_no_variant_attributes().with_prefetch(self._prefetch_ids)
|
||
|
ptavs = ptavs._filter_single_value_lines().with_prefetch(self._prefetch_ids)
|
||
|
return ", ".join([ptav.name for ptav in ptavs])
|
||
|
|
||
|
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)]")
|
||
|
|
||
|
@api.model_create_multi
|
||
|
def create(self, vals_list):
|
||
|
exclusions = super().create(vals_list)
|
||
|
exclusions.product_tmpl_id._create_variant_ids()
|
||
|
return exclusions
|
||
|
|
||
|
def unlink(self):
|
||
|
# Keep a reference to the related templates before the deletion.
|
||
|
templates = self.product_tmpl_id
|
||
|
res = super().unlink()
|
||
|
templates._create_variant_ids()
|
||
|
return res
|
||
|
|
||
|
def write(self, values):
|
||
|
templates = self.env['product.template']
|
||
|
if 'product_tmpl_id' in values:
|
||
|
templates = self.product_tmpl_id
|
||
|
res = super().write(values)
|
||
|
(templates | self.product_tmpl_id)._create_variant_ids()
|
||
|
return res
|
||
|
|
||
|
|
||
|
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
|