mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-26 11:52:06 +02:00
Initial commit: Sale packages
This commit is contained in:
commit
14e3d26998
6469 changed files with 2479670 additions and 0 deletions
|
|
@ -0,0 +1,644 @@
|
|||
# -*- 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.with_context(active_test=False).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
|
||||
Loading…
Add table
Add a link
Reference in a new issue