Permalink
Browse files

[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"
  • Loading branch information...
awa-odoo committed Sep 18, 2018
1 parent 1b96444 commit bc9a3998adaa8198fce33427b47a802782f47e16
Showing with 1,015 additions and 4,749 deletions.
  1. +7 −1 addons/product/models/product.py
  2. +8 −2 addons/product/models/product_attribute.py
  3. +22 −8 addons/product/models/product_template.py
  4. +1 −1 addons/product/tests/test_variants.py
  5. +1 −1 addons/product/views/product_attribute_views.xml
  6. +2 −1 addons/product/views/product_views.xml
  7. +105 −53 addons/sale/controllers/product_configurator.py
  8. +17 −0 addons/sale/models/product_template.py
  9. +21 −1 addons/sale/models/sale.py
  10. +83 −20 addons/sale/static/src/js/product_configurator_controller.js
  11. +256 −195 addons/sale/static/src/js/product_configurator_mixin.js
  12. +233 −129 addons/sale/static/src/js/product_configurator_modal.js
  13. +22 −14 addons/sale/views/sale_product_configurator_templates.xml
  14. +1 −0 addons/sale/views/sale_views.xml
  15. +0 −2 addons/sale_management/__manifest__.py
  16. +32 −22 addons/website_sale/controllers/main.py
  17. +0 −6 addons/website_sale/data/demo.xml
  18. +14 −17 addons/website_sale/models/sale_order.py
  19. +2 −10 addons/website_sale/static/src/js/website_sale.js
  20. +74 −21 addons/website_sale/static/src/js/website_sale_options.js
  21. +3 −2 addons/website_sale/views/templates.xml
  22. +0 −160 addons/website_sale_options/i18n/ar.po
  23. +0 −158 addons/website_sale_options/i18n/cs.po
  24. +0 −162 addons/website_sale_options/i18n/de.po
  25. +0 −156 addons/website_sale_options/i18n/el.po
  26. +0 −162 addons/website_sale_options/i18n/es.po
  27. +0 −160 addons/website_sale_options/i18n/fa.po
  28. +0 −161 addons/website_sale_options/i18n/fi.po
  29. +0 −169 addons/website_sale_options/i18n/fr.po
  30. +0 −156 addons/website_sale_options/i18n/gu.po
  31. +0 −158 addons/website_sale_options/i18n/it.po
  32. +0 −156 addons/website_sale_options/i18n/km.po
  33. +0 −160 addons/website_sale_options/i18n/mn.po
  34. +0 −160 addons/website_sale_options/i18n/nb.po
  35. +0 −164 addons/website_sale_options/i18n/nl.po
  36. +0 −156 addons/website_sale_options/i18n/pl.po
  37. +0 −161 addons/website_sale_options/i18n/pt_BR.po
  38. +0 −157 addons/website_sale_options/i18n/ro.po
  39. +0 −161 addons/website_sale_options/i18n/ru.po
  40. +0 −156 addons/website_sale_options/i18n/sr.po
  41. +0 −163 addons/website_sale_options/i18n/tr.po
  42. +0 −162 addons/website_sale_options/i18n/uk.po
  43. +0 −156 addons/website_sale_options/i18n/vi.po
  44. +0 −146 addons/website_sale_options/i18n/website_sale_options.pot
  45. +0 −161 addons/website_sale_options/i18n/zh_CN.po
  46. +0 −157 addons/website_sale_options/i18n/zh_TW.po
  47. +0 −158 addons/website_sale_options/views/website_sale_options_templates.xml
  48. +25 −17 addons/website_sale_stock/controllers/main.py
  49. +84 −0 addons/website_sale_stock/static/src/js/product_configurator_mixin.js
  50. +0 −88 addons/website_sale_stock/static/src/js/website_sale_stock.js
  51. +1 −1 addons/website_sale_stock/views/website_sale_stock_templates.xml
  52. +1 −1 addons/website_sale_wishlist/static/src/js/website_sale_wishlist.js
@@ -292,7 +292,7 @@ def _check_attribute_value_ids(self):
for value in product.attribute_value_ids:
if value.attribute_id in attributes:
raise ValidationError(_('Error! It is not allowed to choose more than one value for a given attribute.'))
if value.attribute_id.create_variant:
if value.attribute_id.create_variant == 'always':
attributes |= value.attribute_id
return True
@@ -531,6 +531,11 @@ def price_compute(self, price_type, uom=False, currency=False, company=False):
prices[product.id] = product[price_type] or 0.0
if price_type == 'list_price':
prices[product.id] += product.price_extra
# we need to add the price from the attributes that do not generate variants
# (see field product.attribute create_variant)
if self._context.get('no_variant_attributes_price_extra'):
# we have a list of price_extra that comes from the attribute values, we need to sum all that
prices[product.id] += sum(self._context.get('no_variant_attributes_price_extra'))
if uom:
prices[product.id] = product.uom_id._compute_price(prices[product.id], uom)
@@ -583,6 +588,7 @@ def get_product_multiline_description_sale(self):
name = self.display_name
if self.description_sale:
name += '\n' + self.description_sale
return name
@@ -16,7 +16,13 @@ class ProductAttribute(models.Model):
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.Boolean(default=True, help="Check this if you want to create multiple variants for this attribute.")
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):
@@ -79,7 +85,7 @@ class ProductAttributeFilterLine(models.Model):
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)]")
string='Attribute Values', domain="[('product_tmpl_id', '=', product_tmpl_id)]")
class ProductAttributeLine(models.Model):
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import itertools
import operator
import psycopg2
from odoo.addons import decimal_precision as dp
@@ -168,6 +169,15 @@ def _compute_currency_id(self):
@api.multi
def _compute_template_price(self):
prices = self._compute_template_price_no_inverse()
for template in self:
template.price = prices.get(template.id, 0.0)
@api.multi
def _compute_template_price_no_inverse(self):
"""The _compute_template_price writes the 'list_price' field with an inverse method
This method allows computing the price without writing the 'list_price'
"""
prices = {}
pricelist_id_or_name = self._context.get('pricelist')
if pricelist_id_or_name:
@@ -188,8 +198,7 @@ def _compute_template_price(self):
partners = [partner] * len(self)
prices = pricelist.get_products_price(self, quantities, partners)
for template in self:
template.price = prices.get(template.id, 0.0)
return prices
@api.multi
def _set_template_price(self):
@@ -412,6 +421,11 @@ def price_compute(self, price_type, uom=False, currency=False, company=False):
prices = dict.fromkeys(self.ids, 0.0)
for template in templates:
prices[template.id] = template[price_type] or 0.0
# yes, there can be attribute values for product template if it's not a variant YET
# (see field product.attribute create_variant)
if price_type == 'list_price' and self._context.get('current_attributes_price_extra'):
# we have a list of price_extra that comes from the attribute values, we need to sum all that
prices[template.id] += sum(self._context.get('current_attributes_price_extra'))
if uom:
prices[template.id] = template.uom_id._compute_price(prices[template.id], uom)
@@ -440,33 +454,33 @@ def create_variant_ids(self):
for tmpl_id in self.with_context(active_test=False):
# adding an attribute with only one value should not recreate product
# write this attribute on every product to make sure we don't lose them
variant_alone = tmpl_id.attribute_line_ids.filtered(lambda line: line.attribute_id.create_variant and len(line.value_ids) == 1).mapped('value_ids')
variant_alone = tmpl_id.attribute_line_ids.filtered(lambda line: line.attribute_id.create_variant == 'always' and len(line.value_ids) == 1).mapped('value_ids')
for value_id in variant_alone:
updated_products = tmpl_id.product_variant_ids.filtered(lambda product: value_id.attribute_id not in product.mapped('attribute_value_ids.attribute_id'))
updated_products.write({'attribute_value_ids': [(4, value_id.id)]})
# iterator of n-uple of product.attribute.value *ids*
variant_matrix = [
AttributeValues.browse(value_ids)
for value_ids in itertools.product(*(line.value_ids.ids for line in tmpl_id.attribute_line_ids if line.value_ids[:1].attribute_id.create_variant))
for value_ids in itertools.product(*(line.value_ids.ids for line in tmpl_id.attribute_line_ids if line.value_ids[:1].attribute_id.create_variant != 'never'))
]
# get the value (id) sets of existing variants
existing_variants = {frozenset(variant.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant).ids) for variant in tmpl_id.product_variant_ids}
existing_variants = {frozenset(variant.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant != 'never').ids) for variant in tmpl_id.product_variant_ids}
# -> for each value set, create a recordset of values to create a
# variant for if the value set isn't already a variant
for value_ids in variant_matrix:
if set(value_ids.ids) not in existing_variants:
if set(value_ids.ids) not in existing_variants and not any(value_id.attribute_id.create_variant == 'dynamic' for value_id in value_ids):
variants_to_create.append({
'product_tmpl_id': tmpl_id.id,
'attribute_value_ids': [(6, 0, value_ids.ids)]
})
# check product
for product_id in tmpl_id.product_variant_ids:
if not product_id.active and product_id.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant) in variant_matrix:
if not product_id.active and product_id.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant != 'never') in variant_matrix:
variants_to_activate.append(product_id)
elif product_id.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant) not in variant_matrix:
elif product_id.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant != 'never') not in variant_matrix:
variants_to_unlink.append(product_id)
if variants_to_activate:
@@ -169,7 +169,7 @@ def setUp(self):
super(TestVariantsNoCreate, self).setUp()
self.size = self.env['product.attribute'].create({
'name': 'Size',
'create_variant': False,
'create_variant': 'never',
'value_ids': [(0, 0, {'name': 'S'}), (0, 0, {'name': 'M'}), (0, 0, {'name': 'L'})],
})
self.size_S = self.size.value_ids[0]
@@ -38,7 +38,7 @@
<field name="name">Product Variant Values</field>
<field name="res_model">product.product.attribute.value</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('product_tmpl_id', '=', active_id), ('attribute_id.create_variant', '=', True)]</field>
<field name="domain">[('product_tmpl_id', '=', active_id)]</field>
<field name="view_ids"
eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('product.product_product_attribute_value_view_tree')}),
@@ -11,12 +11,13 @@
<header>
<button string="Configure Variants" type="action"
name="%(product_attribute_value_action)d"
attrs="{'invisible': [('product_variant_count', '&lt;=', 1)]}"
attrs="{'invisible': [('attribute_line_ids', '=', False)]}"
groups="product.group_product_variant"/>
</header>
<sheet>
<field name='product_variant_count' invisible='1'/>
<field name='is_product_variant' invisible='1'/>
<field name='attribute_line_ids' invisible='1'/>
<field name="id" invisible="True"/>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object"
Oops, something went wrong.

0 comments on commit bc9a399

Please sign in to comment.