mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-04 04:39:11 +00:00
chore: Removed Shopping Cart Module
- Moved all files and web templates from Shopping Cart to E-commerce module - Made Shopping Cart module obsolete - Moved select E-commerce related files from Portal to E-commerce module - Minor cleanups - Fixed Shopping Cart and Product Configurator tests
This commit is contained in:
@@ -1,114 +0,0 @@
|
||||
import frappe
|
||||
|
||||
|
||||
class ItemVariantsCacheManager:
|
||||
def __init__(self, item_code):
|
||||
self.item_code = item_code
|
||||
|
||||
def get_item_variants_data(self):
|
||||
val = frappe.cache().hget('item_variants_data', self.item_code)
|
||||
|
||||
if not val:
|
||||
self.build_cache()
|
||||
|
||||
return frappe.cache().hget('item_variants_data', self.item_code)
|
||||
|
||||
|
||||
def get_attribute_value_item_map(self):
|
||||
val = frappe.cache().hget('attribute_value_item_map', self.item_code)
|
||||
|
||||
if not val:
|
||||
self.build_cache()
|
||||
|
||||
return frappe.cache().hget('attribute_value_item_map', self.item_code)
|
||||
|
||||
|
||||
def get_item_attribute_value_map(self):
|
||||
val = frappe.cache().hget('item_attribute_value_map', self.item_code)
|
||||
|
||||
if not val:
|
||||
self.build_cache()
|
||||
|
||||
return frappe.cache().hget('item_attribute_value_map', self.item_code)
|
||||
|
||||
|
||||
def get_optional_attributes(self):
|
||||
val = frappe.cache().hget('optional_attributes', self.item_code)
|
||||
|
||||
if not val:
|
||||
self.build_cache()
|
||||
|
||||
return frappe.cache().hget('optional_attributes', self.item_code)
|
||||
|
||||
def get_ordered_attribute_values(self):
|
||||
val = frappe.cache().get_value('ordered_attribute_values_map')
|
||||
if val: return val
|
||||
|
||||
all_attribute_values = frappe.db.get_all('Item Attribute Value',
|
||||
['attribute_value', 'idx', 'parent'], order_by='idx asc')
|
||||
|
||||
ordered_attribute_values_map = frappe._dict({})
|
||||
for d in all_attribute_values:
|
||||
ordered_attribute_values_map.setdefault(d.parent, []).append(d.attribute_value)
|
||||
|
||||
frappe.cache().set_value('ordered_attribute_values_map', ordered_attribute_values_map)
|
||||
return ordered_attribute_values_map
|
||||
|
||||
def build_cache(self):
|
||||
parent_item_code = self.item_code
|
||||
|
||||
attributes = [a.attribute for a in frappe.db.get_all('Item Variant Attribute',
|
||||
{'parent': parent_item_code}, ['attribute'], order_by='idx asc')
|
||||
]
|
||||
|
||||
item_variants_data = frappe.db.get_all('Item Variant Attribute',
|
||||
{'variant_of': parent_item_code}, ['parent', 'attribute', 'attribute_value'],
|
||||
order_by='name',
|
||||
as_list=1
|
||||
)
|
||||
|
||||
disabled_items = set([i.name for i in frappe.db.get_all('Item', {'disabled': 1})])
|
||||
|
||||
attribute_value_item_map = frappe._dict({})
|
||||
item_attribute_value_map = frappe._dict({})
|
||||
|
||||
item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items]
|
||||
for row in item_variants_data:
|
||||
item_code, attribute, attribute_value = row
|
||||
# (attr, value) => [item1, item2]
|
||||
attribute_value_item_map.setdefault((attribute, attribute_value), []).append(item_code)
|
||||
# item => {attr1: value1, attr2: value2}
|
||||
item_attribute_value_map.setdefault(item_code, {})[attribute] = attribute_value
|
||||
|
||||
optional_attributes = set()
|
||||
for item_code, attr_dict in item_attribute_value_map.items():
|
||||
for attribute in attributes:
|
||||
if attribute not in attr_dict:
|
||||
optional_attributes.add(attribute)
|
||||
|
||||
frappe.cache().hset('attribute_value_item_map', parent_item_code, attribute_value_item_map)
|
||||
frappe.cache().hset('item_attribute_value_map', parent_item_code, item_attribute_value_map)
|
||||
frappe.cache().hset('item_variants_data', parent_item_code, item_variants_data)
|
||||
frappe.cache().hset('optional_attributes', parent_item_code, optional_attributes)
|
||||
|
||||
def clear_cache(self):
|
||||
keys = ['attribute_value_item_map', 'item_attribute_value_map', 'item_variants_data', 'optional_attributes']
|
||||
|
||||
for key in keys:
|
||||
frappe.cache().hdel(key, self.item_code)
|
||||
|
||||
def rebuild_cache(self):
|
||||
self.clear_cache()
|
||||
enqueue_build_cache(self.item_code)
|
||||
|
||||
|
||||
def build_cache(item_code):
|
||||
frappe.cache().hset('item_cache_build_in_progress', item_code, 1)
|
||||
i = ItemVariantsCacheManager(item_code)
|
||||
i.build_cache()
|
||||
frappe.cache().hset('item_cache_build_in_progress', item_code, 0)
|
||||
|
||||
def enqueue_build_cache(item_code):
|
||||
if frappe.cache().hget('item_cache_build_in_progress', item_code):
|
||||
return
|
||||
frappe.enqueue(build_cache, item_code=item_code, queue='long')
|
||||
@@ -1,145 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from bs4 import BeautifulSoup
|
||||
from frappe.utils import get_html_for_route
|
||||
|
||||
from erpnext.portal.product_configurator.utils import get_products_for_website
|
||||
|
||||
test_dependencies = ["Item"]
|
||||
|
||||
class TestProductConfigurator(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.create_variant_item()
|
||||
|
||||
@classmethod
|
||||
def create_variant_item(cls):
|
||||
if not frappe.db.exists('Item', '_Test Variant Item - 2XL'):
|
||||
frappe.get_doc({
|
||||
"description": "_Test Variant Item - 2XL",
|
||||
"item_code": "_Test Variant Item - 2XL",
|
||||
"item_name": "_Test Variant Item - 2XL",
|
||||
"doctype": "Item",
|
||||
"is_stock_item": 1,
|
||||
"variant_of": "_Test Variant Item",
|
||||
"item_group": "_Test Item Group",
|
||||
"stock_uom": "_Test UOM",
|
||||
"item_defaults": [{
|
||||
"company": "_Test Company",
|
||||
"default_warehouse": "_Test Warehouse - _TC",
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"buying_cost_center": "_Test Cost Center - _TC",
|
||||
"selling_cost_center": "_Test Cost Center - _TC",
|
||||
"income_account": "Sales - _TC"
|
||||
}],
|
||||
"attributes": [
|
||||
{
|
||||
"attribute": "Test Size",
|
||||
"attribute_value": "2XL"
|
||||
}
|
||||
],
|
||||
"show_variant_in_website": 1
|
||||
}).insert()
|
||||
|
||||
def create_regular_web_item(self, name, item_group=None):
|
||||
if not frappe.db.exists('Item', name):
|
||||
doc = frappe.get_doc({
|
||||
"description": name,
|
||||
"item_code": name,
|
||||
"item_name": name,
|
||||
"doctype": "Item",
|
||||
"is_stock_item": 1,
|
||||
"item_group": item_group or "_Test Item Group",
|
||||
"stock_uom": "_Test UOM",
|
||||
"item_defaults": [{
|
||||
"company": "_Test Company",
|
||||
"default_warehouse": "_Test Warehouse - _TC",
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"buying_cost_center": "_Test Cost Center - _TC",
|
||||
"selling_cost_center": "_Test Cost Center - _TC",
|
||||
"income_account": "Sales - _TC"
|
||||
}],
|
||||
"show_in_website": 1
|
||||
}).insert()
|
||||
else:
|
||||
doc = frappe.get_doc("Item", name)
|
||||
return doc
|
||||
|
||||
def test_product_list(self):
|
||||
template_items = frappe.get_all('Item', {'show_in_website': 1})
|
||||
variant_items = frappe.get_all('Item', {'show_variant_in_website': 1})
|
||||
|
||||
e_commerce_settings = frappe.get_doc('E Commerce Settings')
|
||||
e_commerce_settings.enable_field_filters = 1
|
||||
e_commerce_settings.append('filter_fields', {'fieldname': 'item_group'})
|
||||
e_commerce_settings.append('filter_fields', {'fieldname': 'stock_uom'})
|
||||
e_commerce_settings.save()
|
||||
|
||||
html = get_html_for_route('all-products')
|
||||
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
products_list = soup.find(class_='products-list')
|
||||
items = products_list.find_all(class_='card')
|
||||
self.assertEqual(len(items), len(template_items + variant_items))
|
||||
|
||||
items_with_item_group = frappe.get_all('Item', {'item_group': '_Test Item Group Desktops', 'show_in_website': 1})
|
||||
variants_with_item_group = frappe.get_all('Item', {'item_group': '_Test Item Group Desktops', 'show_variant_in_website': 1})
|
||||
|
||||
# mock query params
|
||||
frappe.form_dict = frappe._dict({
|
||||
'field_filters': '{"item_group":["_Test Item Group Desktops"]}'
|
||||
})
|
||||
html = get_html_for_route('all-products')
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
products_list = soup.find(class_='products-list')
|
||||
items = products_list.find_all(class_='card')
|
||||
self.assertEqual(len(items), len(items_with_item_group + variants_with_item_group))
|
||||
|
||||
|
||||
def test_get_products_for_website(self):
|
||||
items = get_products_for_website(attribute_filters={
|
||||
'Test Size': ['2XL']
|
||||
})
|
||||
self.assertEqual(len(items), 1)
|
||||
|
||||
def test_products_in_multiple_item_groups(self):
|
||||
"""Check if product is visible on multiple item group pages barring its own."""
|
||||
from erpnext.shopping_cart.product_query import ProductQuery
|
||||
|
||||
if not frappe.db.exists("Item Group", {"name": "Tech Items"}):
|
||||
item_group_doc = frappe.get_doc({
|
||||
"doctype": "Item Group",
|
||||
"item_group_name": "Tech Items",
|
||||
"parent_item_group": "All Item Groups",
|
||||
"show_in_website": 1
|
||||
}).insert()
|
||||
else:
|
||||
item_group_doc = frappe.get_doc("Item Group", "Tech Items")
|
||||
|
||||
doc = self.create_regular_web_item("Portal Item", item_group="Tech Items")
|
||||
if not frappe.db.exists("Website Item Group", {"parent": "Portal Item"}):
|
||||
doc.append("website_item_groups", {
|
||||
"item_group": "_Test Item Group Desktops"
|
||||
})
|
||||
doc.save()
|
||||
|
||||
# check if item is visible in its own Item Group's page
|
||||
engine = ProductQuery()
|
||||
items = engine.query({}, {"item_group": "Tech Items"}, None, start=0, item_group="Tech Items")
|
||||
self.assertEqual(len(items), 1)
|
||||
self.assertEqual(items[0].item_code, "Portal Item")
|
||||
|
||||
# check if item is visible in configured foreign Item Group's page
|
||||
engine = ProductQuery()
|
||||
items = engine.query({}, {"item_group": "_Test Item Group Desktops"}, None, start=0, item_group="_Test Item Group Desktops")
|
||||
item_codes = [row.item_code for row in items]
|
||||
|
||||
self.assertIn(len(items), [2, 3])
|
||||
self.assertIn("Portal Item", item_codes)
|
||||
|
||||
# teardown
|
||||
doc.delete()
|
||||
item_group_doc.delete()
|
||||
@@ -1,140 +0,0 @@
|
||||
import frappe
|
||||
from frappe.utils import cint
|
||||
|
||||
from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager
|
||||
from erpnext.setup.doctype.item_group.item_group import get_child_groups
|
||||
from erpnext.shopping_cart.product_info import get_product_info_for_website
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_attributes_and_values(item_code):
|
||||
'''Build a list of attributes and their possible values.
|
||||
This will ignore the values upon selection of which there cannot exist one item.
|
||||
'''
|
||||
item_cache = ItemVariantsCacheManager(item_code)
|
||||
item_variants_data = item_cache.get_item_variants_data()
|
||||
|
||||
attributes = get_item_attributes(item_code)
|
||||
attribute_list = [a.attribute for a in attributes]
|
||||
|
||||
valid_options = {}
|
||||
for item_code, attribute, attribute_value in item_variants_data:
|
||||
if attribute in attribute_list:
|
||||
valid_options.setdefault(attribute, set()).add(attribute_value)
|
||||
|
||||
item_attribute_values = frappe.db.get_all('Item Attribute Value',
|
||||
['parent', 'attribute_value', 'idx'], order_by='parent asc, idx asc')
|
||||
ordered_attribute_value_map = frappe._dict()
|
||||
for iv in item_attribute_values:
|
||||
ordered_attribute_value_map.setdefault(iv.parent, []).append(iv.attribute_value)
|
||||
|
||||
# build attribute values in idx order
|
||||
for attr in attributes:
|
||||
valid_attribute_values = valid_options.get(attr.attribute, [])
|
||||
ordered_values = ordered_attribute_value_map.get(attr.attribute, [])
|
||||
attr['values'] = [v for v in ordered_values if v in valid_attribute_values]
|
||||
|
||||
return attributes
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_next_attribute_and_values(item_code, selected_attributes):
|
||||
'''Find the count of Items that match the selected attributes.
|
||||
Also, find the attribute values that are not applicable for further searching.
|
||||
If less than equal to 10 items are found, return item_codes of those items.
|
||||
If one item is matched exactly, return item_code of that item.
|
||||
'''
|
||||
selected_attributes = frappe.parse_json(selected_attributes)
|
||||
|
||||
item_cache = ItemVariantsCacheManager(item_code)
|
||||
item_variants_data = item_cache.get_item_variants_data()
|
||||
|
||||
attributes = get_item_attributes(item_code)
|
||||
attribute_list = [a.attribute for a in attributes]
|
||||
filtered_items = get_items_with_selected_attributes(item_code, selected_attributes)
|
||||
|
||||
next_attribute = None
|
||||
|
||||
for attribute in attribute_list:
|
||||
if attribute not in selected_attributes:
|
||||
next_attribute = attribute
|
||||
break
|
||||
|
||||
valid_options_for_attributes = frappe._dict({})
|
||||
|
||||
for a in attribute_list:
|
||||
valid_options_for_attributes[a] = set()
|
||||
|
||||
selected_attribute = selected_attributes.get(a, None)
|
||||
if selected_attribute:
|
||||
# already selected attribute values are valid options
|
||||
valid_options_for_attributes[a].add(selected_attribute)
|
||||
|
||||
for row in item_variants_data:
|
||||
item_code, attribute, attribute_value = row
|
||||
if item_code in filtered_items and attribute not in selected_attributes and attribute in attribute_list:
|
||||
valid_options_for_attributes[attribute].add(attribute_value)
|
||||
|
||||
optional_attributes = item_cache.get_optional_attributes()
|
||||
exact_match = []
|
||||
# search for exact match if all selected attributes are required attributes
|
||||
if len(selected_attributes.keys()) >= (len(attribute_list) - len(optional_attributes)):
|
||||
item_attribute_value_map = item_cache.get_item_attribute_value_map()
|
||||
for item_code, attr_dict in item_attribute_value_map.items():
|
||||
if item_code in filtered_items and set(attr_dict.keys()) == set(selected_attributes.keys()):
|
||||
exact_match.append(item_code)
|
||||
|
||||
filtered_items_count = len(filtered_items)
|
||||
|
||||
# get product info if exact match
|
||||
from erpnext.shopping_cart.product_info import get_product_info_for_website
|
||||
if exact_match:
|
||||
data = get_product_info_for_website(exact_match[0])
|
||||
product_info = data.product_info
|
||||
if product_info:
|
||||
product_info["allow_items_not_in_stock"] = cint(data.cart_settings.allow_items_not_in_stock)
|
||||
if not data.cart_settings.show_price:
|
||||
product_info = None
|
||||
else:
|
||||
product_info = None
|
||||
|
||||
return {
|
||||
'next_attribute': next_attribute,
|
||||
'valid_options_for_attributes': valid_options_for_attributes,
|
||||
'filtered_items_count': filtered_items_count,
|
||||
'filtered_items': filtered_items if filtered_items_count < 10 else [],
|
||||
'exact_match': exact_match,
|
||||
'product_info': product_info
|
||||
}
|
||||
|
||||
|
||||
def get_items_with_selected_attributes(item_code, selected_attributes):
|
||||
item_cache = ItemVariantsCacheManager(item_code)
|
||||
attribute_value_item_map = item_cache.get_attribute_value_item_map()
|
||||
|
||||
items = []
|
||||
for attribute, value in selected_attributes.items():
|
||||
filtered_items = attribute_value_item_map.get((attribute, value), [])
|
||||
items.append(set(filtered_items))
|
||||
|
||||
return set.intersection(*items)
|
||||
|
||||
# utilities
|
||||
|
||||
def get_item_attributes(item_code):
|
||||
attributes = frappe.db.get_all('Item Variant Attribute',
|
||||
fields=['attribute'],
|
||||
filters={
|
||||
'parenttype': 'Item',
|
||||
'parent': item_code
|
||||
},
|
||||
order_by='idx asc'
|
||||
)
|
||||
|
||||
optional_attributes = ItemVariantsCacheManager(item_code).get_optional_attributes()
|
||||
|
||||
for a in attributes:
|
||||
if a.attribute in optional_attributes:
|
||||
a.optional = True
|
||||
|
||||
return attributes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import frappe
|
||||
from frappe.utils.nestedset import get_root_of
|
||||
|
||||
from erpnext.shopping_cart.cart import get_debtors_account
|
||||
from erpnext.e_commerce.shopping_cart.cart import get_debtors_account
|
||||
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||
get_shopping_cart_settings,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user