mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-22 06:29:20 +00:00
Merge pull request #29219 from marination/item-variant-config-backend-cleanup
fix: Behaviour of Item Variants Cache generation
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
import itertools
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
@@ -203,16 +202,15 @@ class WebsiteItem(WebsiteGenerator):
|
|||||||
context.body_class = "product-page"
|
context.body_class = "product-page"
|
||||||
|
|
||||||
context.parents = get_parent_item_groups(self.item_group, from_item=True) # breadcumbs
|
context.parents = get_parent_item_groups(self.item_group, from_item=True) # breadcumbs
|
||||||
self.attributes = frappe.get_all("Item Variant Attribute",
|
self.attributes = frappe.get_all(
|
||||||
|
"Item Variant Attribute",
|
||||||
fields=["attribute", "attribute_value"],
|
fields=["attribute", "attribute_value"],
|
||||||
filters={"parent": self.item_code})
|
filters={"parent": self.item_code}
|
||||||
|
)
|
||||||
|
|
||||||
if self.slideshow:
|
if self.slideshow:
|
||||||
context.update(get_slideshow(self))
|
context.update(get_slideshow(self))
|
||||||
|
|
||||||
self.set_variant_context(context)
|
|
||||||
self.set_attribute_context(context)
|
|
||||||
self.set_disabled_attributes(context)
|
|
||||||
self.set_metatags(context)
|
self.set_metatags(context)
|
||||||
self.set_shopping_cart_data(context)
|
self.set_shopping_cart_data(context)
|
||||||
|
|
||||||
@@ -237,61 +235,6 @@ class WebsiteItem(WebsiteGenerator):
|
|||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def set_variant_context(self, context):
|
|
||||||
if not self.has_variants:
|
|
||||||
return
|
|
||||||
|
|
||||||
context.no_cache = True
|
|
||||||
variant = frappe.form_dict.variant
|
|
||||||
|
|
||||||
# load variants
|
|
||||||
# also used in set_attribute_context
|
|
||||||
context.variants = frappe.get_all(
|
|
||||||
"Item",
|
|
||||||
filters={
|
|
||||||
"variant_of": self.item_code,
|
|
||||||
"published_in_website": 1
|
|
||||||
},
|
|
||||||
order_by="name asc")
|
|
||||||
|
|
||||||
# the case when the item is opened for the first time from its list
|
|
||||||
if not variant and context.variants:
|
|
||||||
variant = context.variants[0]
|
|
||||||
|
|
||||||
if variant:
|
|
||||||
context.variant = frappe.get_doc("Item", variant)
|
|
||||||
fields = ("website_image", "website_image_alt", "web_long_description", "description",
|
|
||||||
"website_specifications")
|
|
||||||
|
|
||||||
for fieldname in fields:
|
|
||||||
if context.variant.get(fieldname):
|
|
||||||
value = context.variant.get(fieldname)
|
|
||||||
if isinstance(value, list):
|
|
||||||
value = [d.as_dict() for d in value]
|
|
||||||
|
|
||||||
context[fieldname] = value
|
|
||||||
|
|
||||||
if self.slideshow and context.variant and context.variant.slideshow:
|
|
||||||
context.update(get_slideshow(context.variant))
|
|
||||||
|
|
||||||
|
|
||||||
def set_attribute_context(self, context):
|
|
||||||
if not self.has_variants:
|
|
||||||
return
|
|
||||||
|
|
||||||
attribute_values_available = {}
|
|
||||||
context.attribute_values = {}
|
|
||||||
context.selected_attributes = {}
|
|
||||||
|
|
||||||
# load attributes
|
|
||||||
self.set_selected_attributes(context.variants, context, attribute_values_available)
|
|
||||||
|
|
||||||
# filter attributes, order based on attribute table
|
|
||||||
item = frappe.get_cached_doc("Item", self.item_code)
|
|
||||||
self.set_attribute_values(item.attributes, context, attribute_values_available)
|
|
||||||
|
|
||||||
context.variant_info = json.dumps(context.variants)
|
|
||||||
|
|
||||||
def set_selected_attributes(self, variants, context, attribute_values_available):
|
def set_selected_attributes(self, variants, context, attribute_values_available):
|
||||||
for variant in variants:
|
for variant in variants:
|
||||||
variant.attributes = frappe.get_all(
|
variant.attributes = frappe.get_all(
|
||||||
@@ -328,50 +271,6 @@ class WebsiteItem(WebsiteGenerator):
|
|||||||
if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []):
|
if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []):
|
||||||
values.append(attr_value.attribute_value)
|
values.append(attr_value.attribute_value)
|
||||||
|
|
||||||
def set_disabled_attributes(self, context):
|
|
||||||
"""Disable selection options of attribute combinations that do not result in a variant"""
|
|
||||||
|
|
||||||
if not self.attributes or not self.has_variants:
|
|
||||||
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.get(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 set_metatags(self, context):
|
def set_metatags(self, context):
|
||||||
context.metatags = frappe._dict({})
|
context.metatags = frappe._dict({})
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class ItemVariantsCacheManager:
|
|||||||
val = frappe.cache().get_value('ordered_attribute_values_map')
|
val = frappe.cache().get_value('ordered_attribute_values_map')
|
||||||
if val: return val
|
if val: return val
|
||||||
|
|
||||||
all_attribute_values = frappe.db.get_all('Item Attribute Value',
|
all_attribute_values = frappe.get_all('Item Attribute Value',
|
||||||
['attribute_value', 'idx', 'parent'], order_by='idx asc')
|
['attribute_value', 'idx', 'parent'], order_by='idx asc')
|
||||||
|
|
||||||
ordered_attribute_values_map = frappe._dict({})
|
ordered_attribute_values_map = frappe._dict({})
|
||||||
@@ -57,25 +57,34 @@ class ItemVariantsCacheManager:
|
|||||||
def build_cache(self):
|
def build_cache(self):
|
||||||
parent_item_code = self.item_code
|
parent_item_code = self.item_code
|
||||||
|
|
||||||
attributes = [a.attribute for a in frappe.db.get_all('Item Variant Attribute',
|
attributes = [
|
||||||
{'parent': parent_item_code}, ['attribute'], order_by='idx asc')
|
a.attribute for a in frappe.get_all(
|
||||||
|
'Item Variant Attribute',
|
||||||
|
{'parent': parent_item_code},
|
||||||
|
['attribute'],
|
||||||
|
order_by='idx asc'
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
item_variants_data = frappe.db.get_all('Item Variant Attribute',
|
# join with Website Item
|
||||||
{'variant_of': parent_item_code}, ['parent', 'attribute', 'attribute_value'],
|
item_variants_data = frappe.get_all(
|
||||||
|
'Item Variant Attribute',
|
||||||
|
{'variant_of': parent_item_code},
|
||||||
|
['parent', 'attribute', 'attribute_value'],
|
||||||
order_by='name',
|
order_by='name',
|
||||||
as_list=1
|
as_list=1
|
||||||
)
|
)
|
||||||
|
|
||||||
unpublished_items = set([i.item_code for i in frappe.db.get_all('Website Item', filters={'published': 0}, fields=["item_code"])])
|
disabled_items = set(
|
||||||
|
[i.name for i in frappe.db.get_all('Item', {'disabled': 1})]
|
||||||
|
)
|
||||||
|
|
||||||
attribute_value_item_map = frappe._dict({})
|
attribute_value_item_map = frappe._dict()
|
||||||
item_attribute_value_map = frappe._dict({})
|
item_attribute_value_map = frappe._dict()
|
||||||
|
|
||||||
# dont consider variants that are unpublished
|
# dont consider variants that are disabled
|
||||||
# (either have no Website Item or are unpublished in Website Item)
|
# pull all other variants
|
||||||
item_variants_data = [r for r in item_variants_data if r[0] not in unpublished_items]
|
item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items]
|
||||||
item_variants_data = [r for r in item_variants_data if frappe.db.exists("Website Item", {"item_code": r[0]})]
|
|
||||||
|
|
||||||
for row in item_variants_data:
|
for row in item_variants_data:
|
||||||
item_code, attribute, attribute_value = row
|
item_code, attribute, attribute_value = row
|
||||||
|
|||||||
@@ -1,11 +1,96 @@
|
|||||||
# import frappe
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
# from erpnext.e_commerce.product_data_engine.query import ProductQuery
|
import frappe
|
||||||
# from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
|
||||||
|
from erpnext.controllers.item_variant import create_variant
|
||||||
|
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
test_dependencies = ["Item"]
|
test_dependencies = ["Item"]
|
||||||
|
|
||||||
class TestVariantSelector(unittest.TestCase):
|
class TestVariantSelector(unittest.TestCase):
|
||||||
# TODO: Variant Selector Tests
|
|
||||||
pass
|
def setUp(self) -> None:
|
||||||
|
self.template_item = make_item("Test-Tshirt-Temp", {
|
||||||
|
"has_variant": 1,
|
||||||
|
"variant_based_on": "Item Attribute",
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"attribute": "Test Size"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attribute": "Test Colour"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
# create L-R, L-G, M-R, M-G and S-R
|
||||||
|
for size in ("Large", "Medium",):
|
||||||
|
for colour in ("Red", "Green",):
|
||||||
|
variant = create_variant("Test-Tshirt-Temp", {
|
||||||
|
"Test Size": size,
|
||||||
|
"Test Colour": colour
|
||||||
|
})
|
||||||
|
variant.save()
|
||||||
|
|
||||||
|
variant = create_variant("Test-Tshirt-Temp", {
|
||||||
|
"Test Size": "Small",
|
||||||
|
"Test Colour": "Red"
|
||||||
|
})
|
||||||
|
variant.save()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
def test_item_attributes(self):
|
||||||
|
"""
|
||||||
|
Test if the right attributes are fetched in the popup.
|
||||||
|
(Attributes must only come from active items)
|
||||||
|
|
||||||
|
Attribute selection must not be linked to Website Items.
|
||||||
|
"""
|
||||||
|
from erpnext.e_commerce.variant_selector.utils import get_attributes_and_values
|
||||||
|
|
||||||
|
make_website_item(self.template_item) # publish template not variants
|
||||||
|
|
||||||
|
attr_data = get_attributes_and_values("Test-Tshirt-Temp")
|
||||||
|
|
||||||
|
self.assertEqual(attr_data[0]["attribute"], "Test Size")
|
||||||
|
self.assertEqual(attr_data[1]["attribute"], "Test Colour")
|
||||||
|
self.assertEqual(len(attr_data[0]["values"]), 3) # ['Small', 'Medium', 'Large']
|
||||||
|
self.assertEqual(len(attr_data[1]["values"]), 2) # ['Red', 'Green']
|
||||||
|
|
||||||
|
# disable small red tshirt, now there are no small tshirts.
|
||||||
|
# but there are some red tshirts
|
||||||
|
small_variant = frappe.get_doc("Item", "Test-Tshirt-Temp-S-R")
|
||||||
|
small_variant.disabled = 1
|
||||||
|
small_variant.save() # trigger cache rebuild
|
||||||
|
|
||||||
|
attr_data = get_attributes_and_values("Test-Tshirt-Temp")
|
||||||
|
|
||||||
|
# Only L and M attribute values must be fetched since S is disabled
|
||||||
|
self.assertEqual(len(attr_data[0]["values"]), 2) # ['Medium', 'Large']
|
||||||
|
|
||||||
|
# teardown
|
||||||
|
small_variant.disabled = 1
|
||||||
|
small_variant.save()
|
||||||
|
|
||||||
|
def test_next_item_variant_values(self):
|
||||||
|
"""
|
||||||
|
Test if on selecting an attribute value, the next possible values
|
||||||
|
are filtered accordingly.
|
||||||
|
Values that dont apply should not be fetched.
|
||||||
|
E.g.
|
||||||
|
There is a ** Small-Red ** Tshirt. No other colour in this size.
|
||||||
|
On selecting ** Small **, only ** Red ** should be selectable next.
|
||||||
|
"""
|
||||||
|
from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values
|
||||||
|
|
||||||
|
next_values = get_next_attribute_and_values("Test-Tshirt-Temp", selected_attributes={"Test Size": "Small"})
|
||||||
|
next_colours = next_values["valid_options_for_attributes"]["Test Colour"]
|
||||||
|
filtered_items = next_values["filtered_items"]
|
||||||
|
|
||||||
|
self.assertEqual(len(next_colours), 1)
|
||||||
|
self.assertEqual(next_colours.pop(), "Red")
|
||||||
|
self.assertEqual(len(filtered_items), 1)
|
||||||
|
self.assertEqual(filtered_items.pop(), "Test-Tshirt-Temp-S-R")
|
||||||
|
|||||||
Reference in New Issue
Block a user