Skip to content

Commit

Permalink
[IMP] product,(website_)sale: Move product options configurator into …
Browse files Browse the repository at this point in the history
…sale module

Task #1871557

Purpose
=======

- Configure a product from a sales order as easily as in the ecommerce
  The backend uses the same template as the frontend to configure a products and its options

- Display optionnal products of optionnal products in the "sale options" section

- Allow adding exlusions to some combinations of the product and/or
  to some combinations of the optionnal and accessory products

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

PHASE I

1. Improve the "Add to cart" with optional products

- When you add an option to cart, move the product to the cart & show the options of this option
- Don't open the option wizard if no option available (in case this module becomes generic,
  it's better to skip this step when it's not needed)
- Display the options right under their related product in the cart

2. Do not allow to add the product as an optional product for itself
Why ?
- No functional sense
- Creates issues in the add to cart wizard

3. Exclude some attribute combinations within the product or with related options and accessory products

- Rename VARIANT PRICES button -> CONFIGURE VARIANTS
- When you click this button and select a value, open form view rather than inline edition
- Form view of variant values:
	- Attribute
	- Value
	- HTML Color Index
	- Attribute Price Extra
	- Not Compatible with: o2m tab with 2 fields: Product AND Attribute Value (of this product)
	  [m2m tag selection]
	- in any new line, autocomplete the current product by default
	- Tooltip: A list of product and attribute values that you want to exclude for
          this product's attribue value. Also applies on optionnal and accessory products.

4. New option to configure a product in a sales order line

- This option is only visible if the corresponding option is activated in the sales settings
- It opens a wizard to select a product template, configure related attribute values
  and select optional products (based on frontend view)
- Attributes not compatible with other selected attributes should not be selectable
- Must work like in the frontend
  • Loading branch information
awa-odoo committed Sep 25, 2018
1 parent fd07990 commit 856c2e9
Show file tree
Hide file tree
Showing 84 changed files with 2,891 additions and 1,149 deletions.
5 changes: 0 additions & 5 deletions .tx/config
Expand Up @@ -942,11 +942,6 @@ file_filter = addons/website_sale_management/i18n/<lang>.po
source_file = addons/website_sale_management/i18n/website_sale_management.pot
source_lang = en

[odoo-12.website_sale_options]
file_filter = addons/website_sale_options/i18n/<lang>.po
source_file = addons/website_sale_options/i18n/website_sale_options.pot
source_lang = en

[odoo-12.website_sale_stock]
file_filter = addons/website_sale_stock/i18n/<lang>.po
source_file = addons/website_sale_stock/i18n/website_sale_stock.pot
Expand Down
5 changes: 2 additions & 3 deletions addons/point_of_sale/tests/test_frontend.py
Expand Up @@ -31,15 +31,14 @@ def test_01_pos_basic_order(self):
pear = env.ref('point_of_sale.whiteboard')
attribute_value = env['product.attribute.value'].create({
'name': 'add 2',
'product_ids': [(6, 0, [pear.id])],
'attribute_id': env['product.attribute'].create({
'name': 'add 2',
}).id,
})
env['product.attribute.price'].create({
env['product.product.attribute.value'].create({
'product_tmpl_id': pear.product_tmpl_id.id,
'price_extra': 2,
'value_id': attribute_value.id,
'product_attribute_value_id': attribute_value.id,
})

fixed_pricelist = env['product.pricelist'].create({
Expand Down
38 changes: 24 additions & 14 deletions addons/product/data/product_demo.xml
Expand Up @@ -212,16 +212,6 @@
<field name="attribute_line_ids" eval="[(6,0,[ref('product.product_attribute_line_1'), ref('product.product_attribute_line_2')])]"/>
</record>

<record id="product_product_4d" model="product.product">
<field name="active" eval="False"/>
</record>

<record id="product_attribute_price_1" model="product.attribute.price">
<field name="product_tmpl_id" ref="product_product_4_product_template"/>
<field name="value_id" ref="product_attribute_value_2"/>
<field name="price_extra">50.40</field>
</record>

<record id="product_product_5" model="product.product">
<field name="name">Corner Desk Right Sit</field>
<field name="categ_id" ref="product_category_5"/>
Expand Down Expand Up @@ -320,14 +310,34 @@
<field name="attribute_id" ref="product_attribute_1"/>
<field name="value_ids" eval="[(6,0,[ref('product.product_attribute_value_1'), ref('product.product_attribute_value_2')])]"/>
</record>

<record id="product_product_11_product_template" model="product.template">
<field name="attribute_line_ids" eval="[(6,0,[ref('product.product_attribute_line_4')])]"/>
</record>

<record id="product_attribute_price_2" model="product.attribute.price">
<field name="product_tmpl_id" ref="product_product_11_product_template"/>
<field name="value_id" ref="product_attribute_value_2"/>
<!--
Handle automatically created product.product.attribute.value.
Meaning that the combination between the "customizable desk" and the attribute value "black" will be materialized
into a "product.product.attribute.value" with the ref "product.product_product_attribute_value_1".
This will allow setting fields like "price_extra" and "exclude_for"
-->
<function model="ir.model.data" name="_update_xmlids">
<value model="base" eval="[{
'xml_id': 'product.product_product_attribute_value_1',
'record': obj().env.ref('product.product_attribute_line_1').product_value_ids[1],
'noupdate': True,
}, {
'xml_id': 'product.product_product_attribute_value_2',
'record': obj().env.ref('product.product_attribute_line_4').product_value_ids[1],
'noupdate': True,
}]"/>
</function>

<record id="product_product_attribute_value_1" model="product.product.attribute.value">
<field name="price_extra">50.40</field>
</record>

<record id="product_product_attribute_value_2" model="product.product.attribute.value">
<field name="price_extra">6.40</field>
</record>

Expand Down
19 changes: 11 additions & 8 deletions addons/product/models/product.py
Expand Up @@ -93,7 +93,7 @@ class ProductProduct(models.Model):
lst_price = fields.Float(
'Sale Price', compute='_compute_product_lst_price',
digits=dp.get_precision('Product Price'), inverse='_set_product_lst_price',
help="The sale price is managed from the product template. Click on the 'Variant Prices' button to set the extra attribute prices.")
help="The sale price is managed from the product template. Click on the 'Configure Variants' button to set the extra attribute prices.")

default_code = fields.Char('Internal Reference', index=True)
code = fields.Char('Reference', compute='_compute_product_code')
Expand All @@ -110,6 +110,8 @@ class ProductProduct(models.Model):
help="International Article Number used for product identification.")
attribute_value_ids = fields.Many2many(
'product.attribute.value', string='Attributes', ondelete='restrict')
product_attribute_value_ids = fields.Many2many(
'product.product.attribute.value', string='Attribute Values', compute="_compute_product_attribute_value_ids")
# image: all image fields are base64 encoded and PIL-supported
image_variant = fields.Binary(
"Variant Image", attachment=True,
Expand Down Expand Up @@ -193,15 +195,10 @@ def _set_product_lst_price(self):
value -= product.price_extra
product.write({'list_price': value})

@api.depends('attribute_value_ids.price_ids.price_extra', 'attribute_value_ids.price_ids.product_tmpl_id')
@api.depends('product_attribute_value_ids.price_extra')
def _compute_product_price_extra(self):
# TDE FIXME: do a real multi and optimize a bit ?
for product in self:
price_extra = 0.0
for attribute_price in product.mapped('attribute_value_ids.price_ids'):
if attribute_price.product_tmpl_id == product.product_tmpl_id:
price_extra += attribute_price.price_extra
product.price_extra = price_extra
product.price_extra = sum(product.mapped('product_attribute_value_ids.price_extra'))

@api.depends('list_price', 'price_extra')
def _compute_product_lst_price(self):
Expand Down Expand Up @@ -276,6 +273,12 @@ def _set_image_value(self, value):
else:
self.product_tmpl_id.image = image

def _compute_product_attribute_value_ids(self):
for product in self:
product.product_attribute_value_ids = self.env['product.product.attribute.value']._search([
('product_tmpl_id', '=', product.product_tmpl_id.id),
('product_attribute_value_id', 'in', product.attribute_value_ids.ids)])

@api.one
def _get_pricelist_items(self):
self.pricelist_item_ids = self.env['product.pricelist.item'].search([
Expand Down
147 changes: 99 additions & 48 deletions addons/product/models/product_attribute.py
Expand Up @@ -19,92 +19,143 @@ class ProductAttribute(models.Model):
create_variant = fields.Boolean(default=True, help="Check this if you want to create multiple variants for this attribute.")


class ProductAttributevalue(models.Model):
class ProductAttributeValue(models.Model):
_name = "product.attribute.value"
_order = 'attribute_id, sequence, id'
_description = 'Product Attribute Value'

name = fields.Char('Attribute Value', required=True, translate=True)
sequence = fields.Integer('Sequence', help="Determine the display order")
attribute_id = fields.Many2one('product.attribute', 'Attribute', ondelete='cascade', required=True)
product_ids = fields.Many2many('product.product', string='Variants', readonly=True)
price_extra = fields.Float(
'Attribute Price Extra', compute='_compute_price_extra', inverse='_set_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.")
price_ids = fields.One2many('product.attribute.price', 'value_id', 'Attribute Prices', readonly=True)
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 !')
('value_company_uniq', 'unique (name, attribute_id)', 'This attribute value already exists !')
]

@api.one
def _compute_price_extra(self):
if self._context.get('active_id'):
price = self.price_ids.filtered(lambda price: price.product_tmpl_id.id == self._context['active_id'])
self.price_extra = price.price_extra
else:
self.price_extra = 0.0

def _set_price_extra(self):
if not self._context.get('active_id'):
return

AttributePrice = self.env['product.attribute.price']
prices = AttributePrice.search([('value_id', 'in', self.ids), ('product_tmpl_id', '=', self._context['active_id'])])
updated = prices.mapped('value_id')
if prices:
prices.write({'price_extra': self.price_extra})
else:
for value in self - updated:
AttributePrice.create({
'product_tmpl_id': self._context['active_id'],
'value_id': value.id,
'price_extra': self.price_extra,
})

@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 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()
return super(ProductAttributeValue, self).unlink()


class ProductProductAttributeValue(models.Model):
_name = "product.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('product.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="""A list of product and attribute values that you want to exclude for this product's attribue value.
Also applies on optionnal and accessory products.""")

@api.multi
def _variant_name(self, variable_attributes):
return ", ".join([v.name for v in self if v.attribute_id in variable_attributes])
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 ProductAttributePrice(models.Model):
_name = "product.attribute.price"
_description = 'Product Attribute Price'
class ProductAttributeFilterLine(models.Model):
_name = "product.attribute.filter.line"

product_tmpl_id = fields.Many2one('product.template', 'Product Template', ondelete='cascade', required=True)
value_id = fields.Many2one('product.attribute.value', 'Product Attribute Value', ondelete='cascade', required=True)
price_extra = fields.Float('Price Extra', digits=dp.get_precision('Product Price'))
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), ('attribute_id.create_variant', '=', True)]")


class ProductAttributeLine(models.Model):
_name = "product.attribute.line"
_rec_name = 'attribute_id'
_description = 'Product Attribute Line'

product_tmpl_id = fields.Many2one('product.template', 'Product Template', ondelete='cascade', required=True)
attribute_id = fields.Many2one('product.attribute', 'Attribute', ondelete='restrict', required=True)
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")

@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)])

@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
Expand Down

0 comments on commit 856c2e9

Please sign in to comment.