Merge pull request #4232 from anandpdoshi/variant-smart-selection
Numeric attribute selector, smart selection of variant based on attribute combinations
This commit is contained in:
commit
31755b485f
@ -5,6 +5,7 @@ from __future__ import unicode_literals
|
||||
import frappe
|
||||
import json
|
||||
import urllib
|
||||
import itertools
|
||||
from frappe import msgprint, _
|
||||
from frappe.utils import cstr, flt, cint, getdate, now_datetime, formatdate
|
||||
from frappe.website.website_generator import WebsiteGenerator
|
||||
@ -130,6 +131,8 @@ class Item(WebsiteGenerator):
|
||||
|
||||
self.set_attribute_context(context)
|
||||
|
||||
self.set_disabled_attributes(context)
|
||||
|
||||
context.parents = self.get_parents(context)
|
||||
|
||||
return context
|
||||
@ -189,15 +192,63 @@ class Item(WebsiteGenerator):
|
||||
for attr in self.attributes:
|
||||
values = context.attribute_values.setdefault(attr.attribute, [])
|
||||
|
||||
# get list of values defined (for sequence)
|
||||
for attr_value in frappe.db.get_all("Item Attribute Value",
|
||||
fields=["attribute_value"], filters={"parent": attr.attribute}, order_by="idx asc"):
|
||||
if cint(frappe.db.get_value("Item Attribute", attr.attribute, "numeric_values")):
|
||||
for val in sorted(attribute_values_available.get(attr.attribute, []), key=flt):
|
||||
values.append(val)
|
||||
|
||||
if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []):
|
||||
values.append(attr_value.attribute_value)
|
||||
else:
|
||||
# get list of values defined (for sequence)
|
||||
for attr_value in frappe.db.get_all("Item Attribute Value",
|
||||
fields=["attribute_value"], filters={"parent": attr.attribute}, order_by="idx asc"):
|
||||
|
||||
if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []):
|
||||
values.append(attr_value.attribute_value)
|
||||
|
||||
context.variant_info = json.dumps(context.variants)
|
||||
|
||||
def set_disabled_attributes(self, context):
|
||||
"""Disable selection options of attribute combinations that do not result in a variant"""
|
||||
if not self.attributes:
|
||||
return
|
||||
|
||||
context.disabled_attributes = {}
|
||||
attributes = [attr.attribute for attr in self.attributes]
|
||||
|
||||
def find_variant(combination):
|
||||
for variant in context.variants:
|
||||
if len(variant.attributes) < len(attributes):
|
||||
continue
|
||||
|
||||
if "combination" not in variant:
|
||||
ref_combination = []
|
||||
|
||||
for attr in variant.attributes:
|
||||
idx = attributes.index(attr.attribute)
|
||||
ref_combination.insert(idx, attr.attribute_value)
|
||||
|
||||
variant["combination"] = ref_combination
|
||||
|
||||
if not (set(combination) - set(variant["combination"])):
|
||||
# check if the combination is a subset of a variant combination
|
||||
# eg. [Blue, 0.5] is a possible combination if exists [Blue, Large, 0.5]
|
||||
return True
|
||||
|
||||
for i, attr in enumerate(self.attributes):
|
||||
if i==0:
|
||||
continue
|
||||
|
||||
combination_source = []
|
||||
|
||||
# loop through previous attributes
|
||||
for prev_attr in self.attributes[:i]:
|
||||
combination_source.append([context.selected_attributes[prev_attr.attribute]])
|
||||
|
||||
combination_source.append(context.attribute_values[attr.attribute])
|
||||
|
||||
for combination in itertools.product(*combination_source):
|
||||
if not find_variant(combination):
|
||||
context.disabled_attributes.setdefault(attr.attribute, []).append(combination[-1])
|
||||
|
||||
def check_warehouse_is_set_for_stock_item(self):
|
||||
if self.is_stock_item==1 and not self.default_warehouse and frappe.get_all("Warehouse"):
|
||||
frappe.msgprint(_("Default Warehouse is mandatory for stock Item."),
|
||||
|
@ -29,7 +29,8 @@
|
||||
<div class="item-attribute-selectors">
|
||||
{% if has_variants %}
|
||||
{% for d in attributes %}
|
||||
<div class="item-view-attribute"
|
||||
{% if attribute_values[d.attribute] -%}
|
||||
<div class="item-view-attribute {% if (attribute_values[d.attribute] | len)==1 -%} hidden {%- endif %}"
|
||||
style="margin-bottom: 10px;">
|
||||
<h6 class="text-muted">{{ _(d.attribute) }}</h6>
|
||||
<select class="form-control"
|
||||
@ -37,12 +38,17 @@
|
||||
data-attribute="{{ d.attribute }}">
|
||||
{% for value in attribute_values[d.attribute] %}
|
||||
<option value="{{ value }}"
|
||||
{% if selected_attributes and selected_attributes[d.attribute]==value -%} selected {%- endif %}>
|
||||
{% if selected_attributes and selected_attributes[d.attribute]==value -%}
|
||||
selected
|
||||
{%- elif disabled_attributes and value in disabled_attributes.get(d.attribute, []) -%}
|
||||
disabled
|
||||
{%- endif %}>
|
||||
{{ _(value) }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{%- endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -64,8 +64,23 @@ frappe.ready(function() {
|
||||
});
|
||||
});
|
||||
|
||||
$("[itemscope] .item-view-attribute select").on("change", function() {
|
||||
var item_code = encodeURIComponent(get_item_code());
|
||||
$("[itemscope] .item-view-attribute .form-control").on("change", function() {
|
||||
try {
|
||||
var item_code = encodeURIComponent(get_item_code());
|
||||
} catch(e) {
|
||||
// unable to find variant
|
||||
// then chose the closest available one
|
||||
|
||||
var attribute = $(this).attr("data-attribute");
|
||||
var attribute_value = $(this).val()
|
||||
var item_code = update_attribute_selectors(attribute, attribute_value);
|
||||
|
||||
if (!item_code) {
|
||||
msgprint(__("Please select some other value for {0}", [attribute]))
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (window.location.search.indexOf(item_code)!==-1) {
|
||||
return;
|
||||
}
|
||||
@ -83,10 +98,8 @@ var toggle_update_cart = function(qty) {
|
||||
|
||||
function get_item_code() {
|
||||
if(window.variant_info) {
|
||||
attributes = {};
|
||||
$('[itemscope]').find(".item-view-attribute select").each(function() {
|
||||
attributes[$(this).attr('data-attribute')] = $(this).val();
|
||||
});
|
||||
var attributes = get_selected_attributes();
|
||||
|
||||
for(var i in variant_info) {
|
||||
var variant = variant_info[i];
|
||||
var match = true;
|
||||
@ -106,3 +119,53 @@ function get_item_code() {
|
||||
return item_code;
|
||||
}
|
||||
}
|
||||
|
||||
function update_attribute_selectors(selected_attribute, selected_attribute_value) {
|
||||
// find the closest match keeping the selected attribute in focus and get the item code
|
||||
|
||||
var attributes = get_selected_attributes();
|
||||
|
||||
var previous_match_score = 0;
|
||||
var matched;
|
||||
for(var i in variant_info) {
|
||||
var variant = variant_info[i];
|
||||
var match_score = 0;
|
||||
var has_selected_attribute = false;
|
||||
|
||||
console.log(variant);
|
||||
|
||||
for(var j in variant.attributes) {
|
||||
if(attributes[variant.attributes[j].attribute]===variant.attributes[j].attribute_value) {
|
||||
match_score = match_score + 1;
|
||||
|
||||
if (variant.attributes[j].attribute==selected_attribute && variant.attributes[j].attribute_value==selected_attribute_value) {
|
||||
has_selected_attribute = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (has_selected_attribute && (match_score > previous_match_score)) {
|
||||
previous_match_score = match_score;
|
||||
matched = variant;
|
||||
}
|
||||
}
|
||||
|
||||
if (matched) {
|
||||
for (var j in matched.attributes) {
|
||||
var attr = matched.attributes[j];
|
||||
$('[itemscope]')
|
||||
.find(repl('.item-view-attribute .form-control[data-attribute="%(attribute)s"]', attr))
|
||||
.val(attr.attribute_value);
|
||||
}
|
||||
|
||||
return matched.name;
|
||||
}
|
||||
}
|
||||
|
||||
function get_selected_attributes() {
|
||||
var attributes = {};
|
||||
$('[itemscope]').find(".item-view-attribute .form-control").each(function() {
|
||||
attributes[$(this).attr('data-attribute')] = $(this).val();
|
||||
});
|
||||
return attributes;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user