Files
odoo_source/addons/product/models/product_attribute.py
T
Aurélien Warnon 51e51055bf [IMP] (website_)sale: Allow configuring variants creation on demand
Purpose
=======

Product attributes can be configured to create variants "on demand".
This means that product.templates having these attributes will only create product.product variant
when that combination is actually selected in a sale order/in the web shop.

Product attribute values are also improved to handle extra prices and exclusions
in all three modes ("never", "on demand", "always")

Specification
=============

Create variants on demand
- add a new mode to generate variants as they are added to the order/cart
  (needed for big # of combinations managed in the inventory and/or produced in mrp with specific boms)
- replace the boolean by a selection radio
  tooltip for this new field:
	Always:
	  Variants are generated straight away when
	  the attribute values are combined in the product configuration.
	Only when the product is added to a sales order:
	  Variants are generated once the combination is selected in the sales order.
	  This is advised when variants are customized on demand and manufactured as such in Odoo.
	Never:
	  Advised when variants are not needed in Odoo.
	  The selected values just show up in the description of sales order lines.
- ability to define attribute extra prices & exclude combinations in all variant modes
- if at least one attribute of the product in set to "on demand" mode, generate all the variants "on demand"
2018-09-25 18:39:32 +02:00

184 lines
9.6 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.addons import decimal_precision as dp
from odoo.exceptions import UserError, ValidationError
from odoo.osv import expression
class ProductAttribute(models.Model):
_name = "product.attribute"
_description = "Product Attribute"
_order = 'sequence, name'
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")
attribute_line_ids = fields.One2many('product.attribute.line', 'attribute_id', 'Lines')
create_variant = fields.Selection([
('never', 'Never'),
('always', 'Always'),
('dynamic', 'Only when the product is added to a sales order')],
default='always',
string="Create Variants",
help="Check this if you want to create multiple variants for this attribute.", required=True)
class ProductAttributeValue(models.Model):
_name = "product.attribute.value"
_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")
attribute_id = fields.Many2one('product.attribute', string='Attribute', ondelete='cascade', required=True)
_sql_constraints = [
('value_company_uniq', 'unique (name, attribute_id)', 'This attribute value already exists !')
]
@api.multi
def name_get(self):
if not self._context.get('show_attribute', True): # TDE FIXME: not used
return super(ProductAttributeValue, self).name_get()
return [(value.id, "%s: %s" % (value.attribute_id.name, value.name)) for value in self]
@api.multi
def _variant_name(self, variable_attributes):
return ", ".join([v.name for v in self if v.attribute_id in variable_attributes])
@api.multi
def unlink(self):
linked_products = self.env['product.product'].with_context(active_test=False).search([('attribute_value_ids', 'in', self.ids)])
if linked_products:
raise UserError(_('The operation cannot be completed:\nYou are trying to delete an attribute value with a reference on a product variant.'))
return super(ProductAttributeValue, self).unlink()
class ProductProductAttributeValue(models.Model):
_name = "product.product.attribute.value"
_order = 'sequence, attribute_id, id'
_description = 'Product Attribute Value'
name = fields.Char('Value', related="product_attribute_value_id.name")
product_attribute_value_id = fields.Many2one('product.attribute.value', string='Attribute Value', required=True, ondelete='cascade', index=True)
product_tmpl_id = fields.Many2one('product.template', string='Product Template', required=True, ondelete='cascade', index=True)
attribute_id = fields.Many2one('product.attribute', string='Attribute', related="product_attribute_value_id.attribute_id")
sequence = fields.Integer('Sequence', related="product_attribute_value_id.sequence")
price_extra = fields.Float(
string='Attribute Price Extra', default=0.0, digits=dp.get_precision('Product Price'),
help="Price Extra: Extra price for the variant with this attribute value on sale price. eg. 200 price extra, 1000 + 200 = 1200.")
exclude_for = fields.Many2many(
'product.attribute.filter.line', string="Exclude for", relation="product_attribute_value_exclusion",
help="Make this attribute value not compatible with other values of the product or some attribute values of optional and accessory products.")
@api.multi
def name_get(self):
if not self._context.get('show_attribute', True): # TDE FIXME: not used
return super(ProductAttributeValue, self).name_get()
return [(value.id, "%s: %s" % (value.attribute_id.name, value.name)) for value in self]
class ProductAttributeFilterLine(models.Model):
_name = "product.attribute.filter.line"
_description = 'Product Attribute Filter Line'
product_tmpl_id = fields.Many2one('product.template', string='Product Template', ondelete='cascade', required=True)
value_ids = fields.Many2many(
'product.product.attribute.value', relation="product_attr_filter_line_value_ids_rel",
string='Attribute Values', domain="[('product_tmpl_id', '=', product_tmpl_id)]")
class ProductAttributeLine(models.Model):
_name = "product.attribute.line"
_rec_name = 'attribute_id'
_description = 'Product Attribute Line'
product_tmpl_id = fields.Many2one('product.template', string='Product Template', ondelete='cascade', required=True)
attribute_id = fields.Many2one('product.attribute', string='Attribute', ondelete='restrict', required=True)
value_ids = fields.Many2many('product.attribute.value', string='Attribute Values')
product_value_ids = fields.Many2many(
'product.product.attribute.value',
string='Product Attribute Values',
compute="_set_product_value_ids",
store=False
)
@api.constrains('value_ids', 'attribute_id')
def _check_valid_attribute(self):
if any(line.value_ids > line.attribute_id.value_ids for line in self):
raise ValidationError(_('You cannot use this attribute with the following value.'))
return True
@api.model
def create(self, values):
res = super(ProductAttributeLine, self).create(values)
res._update_product_product_attribute_values()
return res
def write(self, values):
res = super(ProductAttributeLine, self).write(values)
self._update_product_product_attribute_values()
return res
@api.depends('value_ids')
def _set_product_value_ids(self):
for product_attribute_line in self:
product_attribute_line.product_value_ids = self.env['product.product.attribute.value'].search([
('product_tmpl_id', 'in', product_attribute_line.product_tmpl_id.ids),
('product_attribute_value_id.attribute_id', 'in', product_attribute_line.value_ids.mapped('attribute_id').ids)]
).sorted(lambda product_product_attribute: product_product_attribute.sequence)
@api.multi
def unlink(self):
for product_attribute_line in self:
self.env['product.product.attribute.value'].search([
('product_tmpl_id', 'in', product_attribute_line.product_tmpl_id.ids),
('product_attribute_value_id.attribute_id', 'in', product_attribute_line.value_ids.mapped('attribute_id').ids)]).unlink()
return super(ProductAttributeLine, self).unlink()
def _update_product_product_attribute_values(self):
"""
Create or unlink product.product.attribute.value based on the attribute lines.
If the product.attribute.value is removed, remove the corresponding product.product.attribute.value
If no product.product.attribute.value exists for the newly added product.attribute.value, create it.
"""
for attribute_line in self:
# All existing product.product.attribute.value for this template
product_product_attribute_values_to_remove = self.env['product.product.attribute.value'].search([
('product_tmpl_id', '=', attribute_line.product_tmpl_id.id),
('product_attribute_value_id.attribute_id', 'in', attribute_line.value_ids.mapped('attribute_id').ids)])
# All existing product.attribute.value shared by all products
# eg (Yellow, Red, Blue, Small, Large)
existing_product_attribute_values = product_product_attribute_values_to_remove.mapped('product_attribute_value_id')
# Loop on product.attribute.values for the line (eg: Yellow, Red, Blue)
for product_attribute_value in attribute_line.value_ids:
if product_attribute_value in existing_product_attribute_values:
# property is already existing: don't touch, remove it from list to avoid unlinking it
product_product_attribute_values_to_remove = product_product_attribute_values_to_remove.filtered(
lambda value: product_attribute_value not in value.mapped('product_attribute_value_id')
)
else:
# property does not exist: create it
self.env['product.product.attribute.value'].create({
'product_attribute_value_id': product_attribute_value.id,
'product_tmpl_id': attribute_line.product_tmpl_id.id})
# at this point, existing properties can be removed to reflect the modifications on value_ids
if product_product_attribute_values_to_remove:
product_product_attribute_values_to_remove.unlink()
@api.model
def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
# TDE FIXME: currently overriding the domain; however as it includes a
# 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 = expression.AND([['|', ('attribute_id', operator, name), ('value_ids', operator, name)], args])
attribute_ids = self._search(args, limit=limit, access_rights_uid=name_get_uid)
return self.browse(attribute_ids).name_get()
return super(ProductAttributeLine, self)._name_search(name=name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid)