Merge branch 'develop' of https://github.com/frappe/erpnext into e-commerce-refactor-develop
diff --git a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py
index bf8c014..9d34cc2 100644
--- a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py
+++ b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py
@@ -41,9 +41,6 @@
"selling_cost_center": "Main - _TC",
"income_account": "Sales - _TC"
}],
- "show_in_website": 1,
- "route":"-test-tesla-car",
- "website_warehouse": "Stores - _TC"
})
item.insert()
# create test item price
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 2c96749..c5b8b54 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -293,7 +293,7 @@
if not status:
return
- shopping_cart_settings = frappe.get_doc("Shopping Cart Settings")
+ shopping_cart_settings = frappe.get_doc("E Commerce Settings")
if status in ["Authorized", "Completed"]:
redirect_to = None
@@ -443,7 +443,7 @@
return get_payment_gateway_account(args.get("payment_gateway_account"))
if args.order_type == "Shopping Cart":
- payment_gateway_account = frappe.get_doc("Shopping Cart Settings").payment_gateway_account
+ payment_gateway_account = frappe.get_doc("E Commerce Settings").payment_gateway_account
return get_payment_gateway_account(payment_gateway_account)
gateway_account = get_payment_gateway_account({"is_default": 1})
diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.py b/erpnext/accounts/doctype/tax_rule/tax_rule.py
index 150498d..9a63dfe 100644
--- a/erpnext/accounts/doctype/tax_rule/tax_rule.py
+++ b/erpnext/accounts/doctype/tax_rule/tax_rule.py
@@ -102,7 +102,7 @@
def validate_use_for_shopping_cart(self):
'''If shopping cart is enabled and no tax rule exists for shopping cart, enable this one'''
if (not self.use_for_shopping_cart
- and cint(frappe.db.get_single_value('Shopping Cart Settings', 'enabled'))
+ and cint(frappe.db.get_single_value('E Commerce Settings', 'enabled'))
and not frappe.db.get_value('Tax Rule', {'use_for_shopping_cart': 1, 'name': ['!=', self.name]})):
self.use_for_shopping_cart = 1
diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py
index 0ab0171..1a34be0 100644
--- a/erpnext/buying/doctype/supplier/supplier.py
+++ b/erpnext/buying/doctype/supplier/supplier.py
@@ -128,28 +128,6 @@
if frappe.defaults.get_global_default('supp_master_name') == 'Supplier Name':
frappe.db.set(self, "supplier_name", newdn)
- def create_onboarding_docs(self, args):
- company = frappe.defaults.get_defaults().get('company') or \
- frappe.db.get_single_value('Global Defaults', 'default_company')
-
- for i in range(1, args.get('max_count')):
- supplier = args.get('supplier_name_' + str(i))
- if supplier:
- try:
- doc = frappe.get_doc({
- 'doctype': self.doctype,
- 'supplier_name': supplier,
- 'supplier_group': _('Local'),
- 'company': company
- }).insert()
-
- if args.get('supplier_email_' + str(i)):
- from erpnext.selling.doctype.customer.customer import create_contact
- create_contact(supplier, 'Supplier',
- doc.name, args.get('supplier_email_' + str(i)))
- except frappe.NameError:
- pass
-
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters):
diff --git a/erpnext/buying/onboarding_slide/add_a_few_suppliers/add_a_few_suppliers.json b/erpnext/buying/onboarding_slide/add_a_few_suppliers/add_a_few_suppliers.json
deleted file mode 100644
index ce3d8cf..0000000
--- a/erpnext/buying/onboarding_slide/add_a_few_suppliers/add_a_few_suppliers.json
+++ /dev/null
@@ -1,49 +0,0 @@
-{
- "add_more_button": 1,
- "app": "ERPNext",
- "creation": "2019-11-15 14:45:32.626641",
- "docstatus": 0,
- "doctype": "Onboarding Slide",
- "domains": [],
- "help_links": [
- {
- "label": "Learn More",
- "video_id": "zsrrVDk6VBs"
- }
- ],
- "idx": 0,
- "image_src": "",
- "is_completed": 0,
- "max_count": 3,
- "modified": "2019-12-09 17:54:18.452038",
- "modified_by": "Administrator",
- "name": "Add A Few Suppliers",
- "owner": "Administrator",
- "ref_doctype": "Supplier",
- "slide_desc": "",
- "slide_fields": [
- {
- "align": "",
- "fieldname": "supplier_name",
- "fieldtype": "Data",
- "label": "Supplier Name",
- "placeholder": "",
- "reqd": 1
- },
- {
- "align": "",
- "fieldtype": "Column Break",
- "reqd": 0
- },
- {
- "align": "",
- "fieldname": "supplier_email",
- "fieldtype": "Data",
- "label": "Supplier Email",
- "reqd": 1
- }
- ],
- "slide_order": 50,
- "slide_title": "Add A Few Suppliers",
- "slide_type": "Create"
-}
\ No newline at end of file
diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py
index 1b56ae9..48483b1 100644
--- a/erpnext/controllers/item_variant.py
+++ b/erpnext/controllers/item_variant.py
@@ -134,7 +134,7 @@
conditions = " or ".join(conditions)
- from erpnext.portal.product_configurator.utils import get_item_codes_by_attributes
+ from erpnext.e_commerce.product_configurator.utils import get_item_codes_by_attributes
possible_variants = [i for i in get_item_codes_by_attributes(args, template) if i != variant_item_code]
for variant in possible_variants:
@@ -264,9 +264,8 @@
def copy_attributes_to_variant(item, variant):
# copy non no-copy fields
- exclude_fields = ["naming_series", "item_code", "item_name", "show_in_website",
- "show_variant_in_website", "opening_stock", "variant_of", "valuation_rate",
- "has_variants", "attributes"]
+ exclude_fields = ["naming_series", "item_code", "item_name", "published_in_website",
+ "opening_stock", "variant_of", "valuation_rate"]
if item.variant_based_on=='Manufacturer':
# don't copy manufacturer values if based on part no
diff --git a/erpnext/demo/data/drug_list.json b/erpnext/demo/data/drug_list.json
index e91c30d..c7c06c9 100644
--- a/erpnext/demo/data/drug_list.json
+++ b/erpnext/demo/data/drug_list.json
@@ -54,7 +54,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -138,7 +137,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -220,7 +218,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -302,7 +299,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -384,7 +380,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -466,7 +461,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -548,7 +542,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -630,7 +623,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -712,7 +704,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -794,7 +785,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -876,7 +866,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -958,7 +947,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1040,7 +1028,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1122,7 +1109,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1204,7 +1190,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1286,7 +1271,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1368,7 +1352,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1450,7 +1433,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1532,7 +1514,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1614,7 +1595,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1696,7 +1676,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1778,7 +1757,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1860,7 +1838,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1942,7 +1919,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2024,7 +2000,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2106,7 +2081,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2188,7 +2162,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2270,7 +2243,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2352,7 +2324,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2434,7 +2405,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2516,7 +2486,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2598,7 +2567,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2680,7 +2648,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2762,7 +2729,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2844,7 +2810,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2926,7 +2891,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3008,7 +2972,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3092,7 +3055,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3174,7 +3136,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3256,7 +3217,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3338,7 +3298,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3420,7 +3379,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3502,7 +3460,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3584,7 +3541,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3666,7 +3622,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3748,7 +3703,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3830,7 +3784,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3912,7 +3865,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3994,7 +3946,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4076,7 +4027,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4158,7 +4108,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4240,7 +4189,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4322,7 +4270,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4404,7 +4351,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4486,7 +4432,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4568,7 +4513,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4650,7 +4594,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4732,7 +4675,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4814,7 +4756,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4896,7 +4837,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4978,7 +4918,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -5060,7 +4999,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -5142,7 +5080,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
- "show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
diff --git a/erpnext/portal/product_configurator/__init__.py b/erpnext/e_commerce/__init__.py
similarity index 100%
copy from erpnext/portal/product_configurator/__init__.py
copy to erpnext/e_commerce/__init__.py
diff --git a/erpnext/portal/product_configurator/__init__.py b/erpnext/e_commerce/doctype/__init__.py
similarity index 100%
copy from erpnext/portal/product_configurator/__init__.py
copy to erpnext/e_commerce/doctype/__init__.py
diff --git a/erpnext/portal/doctype/products_settings/__init__.py b/erpnext/e_commerce/doctype/e_commerce_settings/__init__.py
similarity index 100%
rename from erpnext/portal/doctype/products_settings/__init__.py
rename to erpnext/e_commerce/doctype/e_commerce_settings/__init__.py
diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js
similarity index 60%
rename from erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js
rename to erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js
index b38828e..131a5e4 100644
--- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js
+++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js
@@ -1,7 +1,7 @@
-// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-// License: GNU General Public License v3. See license.txt
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
-frappe.ui.form.on("Shopping Cart Settings", {
+frappe.ui.form.on("E Commerce Settings", {
onload: function(frm) {
if(frm.doc.__onload && frm.doc.__onload.quotation_series) {
frm.fields_dict.quotation_series.df.options = frm.doc.__onload.quotation_series;
@@ -23,6 +23,21 @@
</div>`
);
}
+
+ frappe.model.with_doctype("Item", () => {
+ const item_meta = frappe.get_meta('Item');
+
+ const valid_fields = item_meta.fields.filter(
+ df => ["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden
+ ).map(df => ({ label: df.label, value: df.fieldname }));
+
+ frm.fields_dict.filter_fields.grid.update_docfield_property(
+ 'fieldname', 'fieldtype', 'Select'
+ );
+ frm.fields_dict.filter_fields.grid.update_docfield_property(
+ 'fieldname', 'options', valid_fields
+ );
+ });
},
enabled: function(frm) {
if (frm.doc.enabled === 1) {
diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json
new file mode 100644
index 0000000..36177ff
--- /dev/null
+++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json
@@ -0,0 +1,356 @@
+{
+ "actions": [],
+ "creation": "2021-02-10 17:13:39.139103",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "home_page_is_products",
+ "show_availability_status",
+ "hide_variants",
+ "column_break_4",
+ "products_per_page",
+ "display_settings_section",
+ "show_attachments",
+ "show_price",
+ "show_stock_availability",
+ "enable_variants",
+ "column_break_13",
+ "show_contact_us_button",
+ "show_quantity_in_website",
+ "show_apply_coupon_code_in_website",
+ "allow_items_not_in_stock",
+ "add_ons_section",
+ "enable_wishlist",
+ "column_break_18",
+ "enable_reviews",
+ "section_break_18",
+ "company",
+ "price_list",
+ "enabled",
+ "store_page_docs",
+ "column_break_21",
+ "default_customer_group",
+ "quotation_series",
+ "checkout_settings_section",
+ "enable_checkout",
+ "save_quotations_as_draft",
+ "column_break_27",
+ "payment_gateway_account",
+ "payment_success_url",
+ "filter_categories_section",
+ "enable_field_filters",
+ "filter_fields",
+ "enable_attribute_filters",
+ "filter_attributes",
+ "shop_by_category_section",
+ "slideshow",
+ "item_search_settings_section",
+ "search_index_fields",
+ "show_categories_in_search_autocomplete",
+ "show_brand_line"
+ ],
+ "fields": [
+ {
+ "default": "0",
+ "description": "If checked, the Home page will be the default Item Group for the website",
+ "fieldname": "home_page_is_products",
+ "fieldtype": "Check",
+ "label": "Home Page is Products"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_availability_status",
+ "fieldtype": "Check",
+ "label": "Show Availability Status"
+ },
+ {
+ "default": "6",
+ "fieldname": "products_per_page",
+ "fieldtype": "Int",
+ "label": "Products per Page"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "filter_categories_section",
+ "fieldtype": "Section Break",
+ "label": "Filters and Categories"
+ },
+ {
+ "default": "0",
+ "fieldname": "hide_variants",
+ "fieldtype": "Check",
+ "label": "Hide Variants"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "description": "The field filters will also work as categories in the <b>Shop by Category</b> page.",
+ "fieldname": "enable_field_filters",
+ "fieldtype": "Check",
+ "label": "Enable Field Filters (Categories)"
+ },
+ {
+ "default": "0",
+ "fieldname": "enable_attribute_filters",
+ "fieldtype": "Check",
+ "label": "Enable Attribute Filters"
+ },
+ {
+ "depends_on": "enable_field_filters",
+ "fieldname": "filter_fields",
+ "fieldtype": "Table",
+ "label": "Item Fields",
+ "options": "Website Filter Field"
+ },
+ {
+ "depends_on": "enable_attribute_filters",
+ "fieldname": "filter_attributes",
+ "fieldtype": "Table",
+ "label": "Attributes",
+ "options": "Website Attribute"
+ },
+ {
+ "default": "0",
+ "fieldname": "enabled",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Enable Shopping Cart"
+ },
+ {
+ "depends_on": "doc.enabled",
+ "fieldname": "store_page_docs",
+ "fieldtype": "HTML"
+ },
+ {
+ "fieldname": "display_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Display Settings"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_attachments",
+ "fieldtype": "Check",
+ "label": "Show Public Attachments"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_price",
+ "fieldtype": "Check",
+ "label": "Show Price"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_stock_availability",
+ "fieldtype": "Check",
+ "label": "Show Stock Availability"
+ },
+ {
+ "default": "0",
+ "fieldname": "enable_variants",
+ "fieldtype": "Check",
+ "label": "Enable Variants"
+ },
+ {
+ "fieldname": "column_break_13",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_contact_us_button",
+ "fieldtype": "Check",
+ "label": "Show Contact Us Button"
+ },
+ {
+ "default": "0",
+ "depends_on": "show_stock_availability",
+ "fieldname": "show_quantity_in_website",
+ "fieldtype": "Check",
+ "label": "Show Stock Quantity"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_apply_coupon_code_in_website",
+ "fieldtype": "Check",
+ "label": "Show Apply Coupon Code"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_items_not_in_stock",
+ "fieldtype": "Check",
+ "label": "Allow items not in stock to be added to cart"
+ },
+ {
+ "fieldname": "section_break_18",
+ "fieldtype": "Section Break",
+ "label": "Shopping Cart"
+ },
+ {
+ "depends_on": "enabled",
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "mandatory_depends_on": "eval: doc.enabled === 1",
+ "options": "Company",
+ "remember_last_selected_value": 1
+ },
+ {
+ "depends_on": "enabled",
+ "description": "Prices will not be shown if Price List is not set",
+ "fieldname": "price_list",
+ "fieldtype": "Link",
+ "label": "Price List",
+ "mandatory_depends_on": "eval: doc.enabled === 1",
+ "options": "Price List"
+ },
+ {
+ "fieldname": "column_break_21",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "enabled",
+ "fieldname": "default_customer_group",
+ "fieldtype": "Link",
+ "ignore_user_permissions": 1,
+ "label": "Default Customer Group",
+ "mandatory_depends_on": "eval: doc.enabled === 1",
+ "options": "Customer Group"
+ },
+ {
+ "depends_on": "enabled",
+ "fieldname": "quotation_series",
+ "fieldtype": "Select",
+ "label": "Quotation Series",
+ "mandatory_depends_on": "eval: doc.enabled === 1"
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "eval:doc.enable_checkout",
+ "depends_on": "enabled",
+ "fieldname": "checkout_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Checkout Settings"
+ },
+ {
+ "default": "0",
+ "fieldname": "enable_checkout",
+ "fieldtype": "Check",
+ "label": "Enable Checkout"
+ },
+ {
+ "default": "Orders",
+ "depends_on": "enable_checkout",
+ "description": "After payment completion redirect user to selected page.",
+ "fieldname": "payment_success_url",
+ "fieldtype": "Select",
+ "label": "Payment Success Url",
+ "mandatory_depends_on": "enable_checkout",
+ "options": "\nOrders\nInvoices\nMy Account"
+ },
+ {
+ "fieldname": "column_break_27",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: doc.enable_checkout == 0",
+ "fieldname": "save_quotations_as_draft",
+ "fieldtype": "Check",
+ "label": "Save Quotations as Draft"
+ },
+ {
+ "depends_on": "enable_checkout",
+ "fieldname": "payment_gateway_account",
+ "fieldtype": "Link",
+ "label": "Payment Gateway Account",
+ "mandatory_depends_on": "enable_checkout",
+ "options": "Payment Gateway Account"
+ },
+ {
+ "collapsible": 1,
+ "depends_on": "enable_field_filters",
+ "fieldname": "shop_by_category_section",
+ "fieldtype": "Section Break",
+ "label": "Shop by Category"
+ },
+ {
+ "fieldname": "slideshow",
+ "fieldtype": "Link",
+ "label": "Slideshow",
+ "options": "Website Slideshow"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "add_ons_section",
+ "fieldtype": "Section Break",
+ "label": "Add-ons"
+ },
+ {
+ "default": "0",
+ "fieldname": "enable_wishlist",
+ "fieldtype": "Check",
+ "label": "Enable Wishlist"
+ },
+ {
+ "fieldname": "column_break_18",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "enable_reviews",
+ "fieldtype": "Check",
+ "label": "Enable Reviews and Ratings"
+ },
+ {
+ "fieldname": "search_index_fields",
+ "fieldtype": "Small Text",
+ "label": "Search Index Fields"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "item_search_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Item Search Settings"
+ },
+ {
+ "default": "1",
+ "fieldname": "show_categories_in_search_autocomplete",
+ "fieldtype": "Check",
+ "label": "Show Categories in Search Autocomplete"
+ },
+ {
+ "default": "0",
+ "description": "e.g. \"iPhone 12 by Apple\"",
+ "fieldname": "show_brand_line",
+ "fieldtype": "Check",
+ "label": "Show Brand Line"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2021-05-05 13:41:11.483232",
+ "modified_by": "Administrator",
+ "module": "E-commerce",
+ "name": "E Commerce Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py
new file mode 100644
index 0000000..441e85b
--- /dev/null
+++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py
@@ -0,0 +1,172 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.utils import cint, comma_and
+from frappe import _, msgprint
+from frappe.model.document import Document
+from frappe.utils import get_datetime, get_datetime_str, now_datetime, unique
+
+from erpnext.e_commerce.website_item_indexing import create_website_items_index, ALLOWED_INDEXABLE_FIELDS_SET
+
+class ShoppingCartSetupError(frappe.ValidationError): pass
+
+class ECommerceSettings(Document):
+ def onload(self):
+ self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series")
+
+ def validate(self):
+ if self.home_page_is_products:
+ frappe.db.set_value("Website Settings", None, "home_page", "products")
+ elif frappe.db.get_single_value("Website Settings", "home_page") == 'products':
+ frappe.db.set_value("Website Settings", None, "home_page", "home")
+
+ self.validate_field_filters()
+ self.validate_attribute_filters()
+ self.validate_checkout()
+ self.validate_brand_check()
+ self.validate_search_index_fields()
+
+ if self.enabled:
+ self.validate_exchange_rates_exist()
+
+ frappe.clear_document_cache("E Commerce Settings", "E Commerce Settings")
+
+ def validate_field_filters(self):
+ if not (self.enable_field_filters and self.filter_fields):
+ return
+
+ item_meta = frappe.get_meta("Item")
+ valid_fields = [df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]]
+
+ for f in self.filter_fields:
+ if f.fieldname not in valid_fields:
+ frappe.throw(_("Filter Fields Row #{0}: Fieldname <b>{1}</b> must be of type 'Link' or 'Table MultiSelect'").format(f.idx, f.fieldname))
+
+ def validate_attribute_filters(self):
+ if not (self.enable_attribute_filters and self.filter_attributes):
+ return
+
+ # if attribute filters are enabled, hide_variants should be disabled
+ self.hide_variants = 0
+
+ def validate_checkout(self):
+ if self.enable_checkout and not self.payment_gateway_account:
+ self.enable_checkout = 0
+
+ def validate_search_index_fields(self):
+ if not self.search_index_fields:
+ return
+
+ # Clean up
+ # Remove whitespaces
+ fields = self.search_index_fields.replace(' ', '')
+ # Remove extra ',' and remove duplicates
+ fields = unique(fields.strip(',').split(','))
+
+ # All fields should be indexable
+ if not (set(fields).issubset(ALLOWED_INDEXABLE_FIELDS_SET)):
+ invalid_fields = list(set(fields).difference(ALLOWED_INDEXABLE_FIELDS_SET))
+ num_invalid_fields = len(invalid_fields)
+ invalid_fields = comma_and(invalid_fields)
+
+ if num_invalid_fields > 1:
+ frappe.throw(_("{0} are not valid options for Search Index Field.").format(frappe.bold(invalid_fields)))
+ else:
+ frappe.throw(_("{0} is not a valid option for Search Index Field.").format(frappe.bold(invalid_fields)))
+
+ self.search_index_fields = ','.join(fields)
+
+ def validate_brand_check(self):
+ if self.show_brand_line and not ("brand" in self.search_index_fields):
+ self.search_index_fields += ",brand"
+
+ def validate_exchange_rates_exist(self):
+ """check if exchange rates exist for all Price List currencies (to company's currency)"""
+ company_currency = frappe.get_cached_value('Company', self.company, "default_currency")
+ if not company_currency:
+ msgprint(_("Please specify currency in Company") + ": " + self.company,
+ raise_exception=ShoppingCartSetupError)
+
+ price_list_currency_map = frappe.db.get_values("Price List",
+ [self.price_list], "currency")
+
+ price_list_currency_map = dict(price_list_currency_map)
+
+ # check if all price lists have a currency
+ for price_list, currency in price_list_currency_map.items():
+ if not currency:
+ frappe.throw(_("Currency is required for Price List {0}").format(price_list))
+
+ expected_to_exist = [currency + "-" + company_currency
+ for currency in price_list_currency_map.values()
+ if currency != company_currency]
+
+ # manqala 20/09/2016: set up selection parameters for query from tabCurrency Exchange
+ from_currency = [currency for currency in price_list_currency_map.values() if currency != company_currency]
+ to_currency = company_currency
+ # manqala end
+
+ if expected_to_exist:
+ # manqala 20/09/2016: modify query so that it uses date in the selection from Currency Exchange.
+ # exchange rates defined with date less than the date on which this document is being saved will be selected
+ exists = frappe.db.sql_list("""select CONCAT(from_currency,'-',to_currency) from `tabCurrency Exchange`
+ where from_currency in (%s) and to_currency = "%s" and date <= curdate()""" % (", ".join(["%s"]*len(from_currency)), to_currency), tuple(from_currency))
+ # manqala end
+
+ missing = list(set(expected_to_exist).difference(exists))
+
+ if missing:
+ msgprint(_("Missing Currency Exchange Rates for {0}").format(comma_and(missing)),
+ raise_exception=ShoppingCartSetupError)
+
+ def validate_tax_rule(self):
+ if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart" : 1}, "name"):
+ frappe.throw(frappe._("Set Tax Rule for shopping cart"), ShoppingCartSetupError)
+
+ def get_tax_master(self, billing_territory):
+ tax_master = self.get_name_from_territory(billing_territory, "sales_taxes_and_charges_masters",
+ "sales_taxes_and_charges_master")
+ return tax_master and tax_master[0] or None
+
+ def get_shipping_rules(self, shipping_territory):
+ return self.get_name_from_territory(shipping_territory, "shipping_rules", "shipping_rule")
+
+ def on_change(self):
+ old_doc = self.get_doc_before_save()
+ old_fields = old_doc.search_index_fields
+ new_fields = self.search_index_fields
+
+ # if search index fields get changed
+ if not (new_fields == old_fields):
+ create_website_items_index()
+
+def validate_cart_settings(doc, method):
+ frappe.get_doc("E Commerce Settings", "E Commerce Settings").run_method("validate")
+
+def get_shopping_cart_settings():
+ if not getattr(frappe.local, "shopping_cart_settings", None):
+ frappe.local.shopping_cart_settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
+
+ return frappe.local.shopping_cart_settings
+
+@frappe.whitelist(allow_guest=True)
+def is_cart_enabled():
+ return get_shopping_cart_settings().enabled
+
+def show_quantity_in_website():
+ return get_shopping_cart_settings().show_quantity_in_website
+
+def check_shopping_cart_enabled():
+ if not get_shopping_cart_settings().enabled:
+ frappe.throw(_("You need to enable Shopping Cart"), ShoppingCartSetupError)
+
+def show_attachments():
+ return get_shopping_cart_settings().show_attachments
+
+def home_page_is_products(doc, method):
+ """Called on saving Website Settings."""
+ home_page_is_products = cint(frappe.db.get_single_value("E Commerce Settings", "home_page_is_products"))
+ if home_page_is_products:
+ doc.home_page = "products"
\ No newline at end of file
diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py
new file mode 100644
index 0000000..798529b
--- /dev/null
+++ b/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+import frappe
+import unittest
+
+from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ShoppingCartSetupError
+
+class TestECommerceSettings(unittest.TestCase):
+ def setUp(self):
+ frappe.db.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """)
+
+ def get_cart_settings(self):
+ return frappe.get_doc({"doctype": "E Commerce Settings",
+ "company": "_Test Company"})
+
+ def test_exchange_rate_exists(self):
+ frappe.db.sql("""delete from `tabCurrency Exchange`""")
+
+ cart_settings = self.get_cart_settings()
+ cart_settings.price_list = "_Test Price List Rest of the World"
+ self.assertRaises(ShoppingCartSetupError, cart_settings.validate_exchange_rates_exist)
+
+ from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \
+ currency_exchange_records
+ frappe.get_doc(currency_exchange_records[0]).insert()
+ cart_settings.validate_exchange_rates_exist()
+
+ def test_tax_rule_validation(self):
+ frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
+ frappe.db.commit()
+
+ cart_settings = self.get_cart_settings()
+ cart_settings.enabled = 1
+ if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"):
+ self.assertRaises(ShoppingCartSetupError, cart_settings.validate_tax_rule)
+
+ frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1")
+
+test_dependencies = ["Tax Rule"]
diff --git a/erpnext/portal/doctype/products_settings/__init__.py b/erpnext/e_commerce/doctype/item_review/__init__.py
similarity index 100%
copy from erpnext/portal/doctype/products_settings/__init__.py
copy to erpnext/e_commerce/doctype/item_review/__init__.py
diff --git a/erpnext/e_commerce/doctype/item_review/item_review.js b/erpnext/e_commerce/doctype/item_review/item_review.js
new file mode 100644
index 0000000..a57c370
--- /dev/null
+++ b/erpnext/e_commerce/doctype/item_review/item_review.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Item Review', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/e_commerce/doctype/item_review/item_review.json b/erpnext/e_commerce/doctype/item_review/item_review.json
new file mode 100644
index 0000000..7b6071b
--- /dev/null
+++ b/erpnext/e_commerce/doctype/item_review/item_review.json
@@ -0,0 +1,126 @@
+{
+ "actions": [],
+ "beta": 1,
+ "creation": "2021-03-23 16:47:26.542226",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "website_item",
+ "user",
+ "customer",
+ "column_break_3",
+ "item",
+ "published_on",
+ "reviews_section",
+ "review_title",
+ "rating",
+ "comment"
+ ],
+ "fields": [
+ {
+ "fieldname": "website_item",
+ "fieldtype": "Link",
+ "label": "Website Item",
+ "options": "Website Item"
+ },
+ {
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "User",
+ "options": "User"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "website_item.item_code",
+ "fieldname": "item",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item",
+ "options": "Item",
+ "read_only": 1
+ },
+ {
+ "fieldname": "reviews_section",
+ "fieldtype": "Section Break",
+ "label": "Reviews"
+ },
+ {
+ "fieldname": "rating",
+ "fieldtype": "Rating",
+ "in_list_view": 1,
+ "label": "Rating"
+ },
+ {
+ "fieldname": "comment",
+ "fieldtype": "Small Text",
+ "label": "Comment"
+ },
+ {
+ "fieldname": "review_title",
+ "fieldtype": "Data",
+ "label": "Review Title"
+ },
+ {
+ "fieldname": "customer",
+ "fieldtype": "Link",
+ "label": "Customer",
+ "options": "Customer"
+ },
+ {
+ "fieldname": "published_on",
+ "fieldtype": "Data",
+ "label": "Published on"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-04-02 15:56:00.447950",
+ "modified_by": "Administrator",
+ "module": "E-commerce",
+ "name": "Item Review",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Website Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "report": 1,
+ "role": "Customer",
+ "share": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/e_commerce/doctype/item_review/item_review.py b/erpnext/e_commerce/doctype/item_review/item_review.py
new file mode 100644
index 0000000..772da04
--- /dev/null
+++ b/erpnext/e_commerce/doctype/item_review/item_review.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+from datetime import datetime
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.contacts.doctype.contact.contact import get_contact_name
+from frappe.utils import flt, cint
+from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import get_shopping_cart_settings
+
+class ItemReview(Document):
+ pass
+
+@frappe.whitelist()
+def get_item_reviews(web_item, start, end, data=None):
+ if not data:
+ data = frappe._dict()
+
+ settings = get_shopping_cart_settings()
+
+ if settings and settings.get("enable_reviews"):
+ data.reviews = frappe.db.get_all("Item Review", filters={"website_item": web_item},
+ fields=["*"], limit_start=cint(start), limit_page_length=cint(end))
+
+ rating_data = frappe.db.get_all("Item Review", filters={"website_item": web_item},
+ fields=["avg(rating) as average, count(*) as total"])[0]
+ data.average_rating = flt(rating_data.average, 1)
+ data.average_whole_rating = flt(data.average_rating, 0)
+
+ # get % of reviews per rating
+ reviews_per_rating = []
+ for i in range(1,6):
+ count = frappe.db.get_all("Item Review", filters={"website_item": web_item, "rating": i},
+ fields=["count(*) as count"])[0].count
+
+ percent = flt((count / rating_data.total or 1) * 100, 0) if count else 0
+ reviews_per_rating.append(percent)
+
+ data.reviews_per_rating = reviews_per_rating
+ data.total_reviews = rating_data.total
+
+ return data
+
+@frappe.whitelist()
+def add_item_review(web_item, title, rating, comment=None):
+ """ Add an Item Review by a user if non-existent. """
+ if not frappe.db.exists("Item Review", {"user": frappe.session.user, "website_item": web_item}):
+ doc = frappe.get_doc({
+ "doctype": "Item Review",
+ "user": frappe.session.user,
+ "customer": get_customer(),
+ "website_item": web_item,
+ "item": frappe.db.get_value("Website Item", web_item, "item_code"),
+ "review_title": title,
+ "rating": rating,
+ "comment": comment
+ })
+ doc.published_on = datetime.today().strftime("%d %B %Y")
+ doc.insert()
+
+def get_customer():
+ user = frappe.session.user
+ contact_name = get_contact_name(user)
+ customer = None
+
+ if contact_name:
+ contact = frappe.get_doc('Contact', contact_name)
+ for link in contact.links:
+ if link.link_doctype == "Customer":
+ customer = link.link_name
+ break
+
+ if customer:
+ return frappe.db.get_value("Customer", customer)
+ else:
+ frappe.throw(_("You are not verified to write a review yet. Please contact us for verification."))
\ No newline at end of file
diff --git a/erpnext/e_commerce/doctype/item_review/test_item_review.py b/erpnext/e_commerce/doctype/item_review/test_item_review.py
new file mode 100644
index 0000000..5e6d249
--- /dev/null
+++ b/erpnext/e_commerce/doctype/item_review/test_item_review.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestItemReview(unittest.TestCase):
+ pass
diff --git a/erpnext/portal/doctype/products_settings/__init__.py b/erpnext/e_commerce/doctype/website_item/__init__.py
similarity index 100%
copy from erpnext/portal/doctype/products_settings/__init__.py
copy to erpnext/e_commerce/doctype/website_item/__init__.py
diff --git a/erpnext/e_commerce/doctype/website_item/templates/website_item.html b/erpnext/e_commerce/doctype/website_item/templates/website_item.html
new file mode 100644
index 0000000..db12309
--- /dev/null
+++ b/erpnext/e_commerce/doctype/website_item/templates/website_item.html
@@ -0,0 +1,7 @@
+{% extends "templates/web.html" %}
+
+{% block page_content %}
+<h1>{{ title }}</h1>
+{% endblock %}
+
+<!-- this is a sample default web page template -->
\ No newline at end of file
diff --git a/erpnext/e_commerce/doctype/website_item/templates/website_item_row.html b/erpnext/e_commerce/doctype/website_item/templates/website_item_row.html
new file mode 100644
index 0000000..d7014b4
--- /dev/null
+++ b/erpnext/e_commerce/doctype/website_item/templates/website_item_row.html
@@ -0,0 +1,4 @@
+<div>
+ <a href="{{ doc.route }}">{{ doc.title or doc.name }}</a>
+</div>
+<!-- this is a sample default list template -->
diff --git a/erpnext/e_commerce/doctype/website_item/test_website_item.py b/erpnext/e_commerce/doctype/website_item/test_website_item.py
new file mode 100644
index 0000000..e4386a3
--- /dev/null
+++ b/erpnext/e_commerce/doctype/website_item/test_website_item.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestWebsiteItem(unittest.TestCase):
+ pass
diff --git a/erpnext/e_commerce/doctype/website_item/website_item.js b/erpnext/e_commerce/doctype/website_item/website_item.js
new file mode 100644
index 0000000..741e78f
--- /dev/null
+++ b/erpnext/e_commerce/doctype/website_item/website_item.js
@@ -0,0 +1,24 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Website Item', {
+ onload: function(frm) {
+ // should never check Private
+ frm.fields_dict["website_image"].df.is_private = 0;
+ },
+
+ image: function() {
+ refresh_field("image_view");
+ },
+
+ copy_from_item_group: function(frm) {
+ return frm.call({
+ doc: frm.doc,
+ method: "copy_specification_from_item_group"
+ });
+ },
+
+ set_meta_tags(frm) {
+ frappe.utils.set_meta_tag(frm.doc.route);
+ }
+});
diff --git a/erpnext/e_commerce/doctype/website_item/website_item.json b/erpnext/e_commerce/doctype/website_item/website_item.json
new file mode 100644
index 0000000..f5eb2dd
--- /dev/null
+++ b/erpnext/e_commerce/doctype/website_item/website_item.json
@@ -0,0 +1,370 @@
+{
+ "actions": [],
+ "allow_guest_to_view": 1,
+ "allow_import": 1,
+ "autoname": "naming_series",
+ "creation": "2021-02-09 21:06:14.441698",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "naming_series",
+ "web_item_name",
+ "route",
+ "has_variants",
+ "variant_of",
+ "published",
+ "column_break_3",
+ "item_code",
+ "item_name",
+ "item_group",
+ "stock_uom",
+ "brand",
+ "image",
+ "display_section",
+ "website_image",
+ "website_image_alt",
+ "column_break_13",
+ "slideshow",
+ "thumbnail",
+ "section_break_17",
+ "website_warehouse",
+ "description",
+ "website_specifications",
+ "copy_from_item_group",
+ "column_break_27",
+ "web_long_description",
+ "display_additional_information_section",
+ "show_tabbed_section",
+ "tabs",
+ "offers_section",
+ "offers",
+ "section_break_6",
+ "ranking",
+ "set_meta_tags",
+ "column_break_22",
+ "website_item_groups",
+ "advanced_display_section",
+ "website_content"
+ ],
+ "fields": [
+ {
+ "description": "Website display name",
+ "fetch_from": "item_code.item_name",
+ "fetch_if_empty": 1,
+ "fieldname": "web_item_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Website Item Name",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "label": "Item Code",
+ "options": "Item",
+ "read_only_depends_on": "eval:!doc.__islocal",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "item_code.item_name",
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name",
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break",
+ "label": "Search and SEO"
+ },
+ {
+ "fieldname": "route",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Route",
+ "no_copy": 1
+ },
+ {
+ "description": "Items with higher ranking will be shown higher",
+ "fieldname": "ranking",
+ "fieldtype": "Int",
+ "label": "Ranking"
+ },
+ {
+ "description": "Show a slideshow at the top of the page",
+ "fieldname": "slideshow",
+ "fieldtype": "Link",
+ "label": "Slideshow",
+ "options": "Website Slideshow"
+ },
+ {
+ "description": "Item Image (if not slideshow)",
+ "fieldname": "website_image",
+ "fieldtype": "Attach",
+ "label": "Website Image"
+ },
+ {
+ "description": "Image Alternative Text",
+ "fieldname": "website_image_alt",
+ "fieldtype": "Data",
+ "label": "Image Description"
+ },
+ {
+ "fieldname": "thumbnail",
+ "fieldtype": "Data",
+ "label": "Thumbnail",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_13",
+ "fieldtype": "Column Break"
+ },
+ {
+ "description": "Show Stock availability based on this warehouse.",
+ "fieldname": "website_warehouse",
+ "fieldtype": "Link",
+ "ignore_user_permissions": 1,
+ "label": "Website Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "description": "List this Item in multiple groups on the website.",
+ "fieldname": "website_item_groups",
+ "fieldtype": "Table",
+ "label": "Website Item Groups",
+ "options": "Website Item Group"
+ },
+ {
+ "fieldname": "set_meta_tags",
+ "fieldtype": "Button",
+ "label": "Set Meta Tags"
+ },
+ {
+ "fieldname": "section_break_17",
+ "fieldtype": "Section Break",
+ "label": "Display Information"
+ },
+ {
+ "fieldname": "copy_from_item_group",
+ "fieldtype": "Button",
+ "label": "Copy From Item Group"
+ },
+ {
+ "fieldname": "website_specifications",
+ "fieldtype": "Table",
+ "label": "Website Specifications",
+ "options": "Item Website Specification"
+ },
+ {
+ "fieldname": "web_long_description",
+ "fieldtype": "Text Editor",
+ "label": "Website Description"
+ },
+ {
+ "description": "You can use any valid Bootstrap 4 markup in this field. It will be shown on your Item Page.",
+ "fieldname": "website_content",
+ "fieldtype": "HTML Editor",
+ "label": "Website Content"
+ },
+ {
+ "fetch_from": "item_code.item_group",
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item Group",
+ "options": "Item Group",
+ "read_only": 1
+ },
+ {
+ "fieldname": "image",
+ "fieldtype": "Attach Image",
+ "hidden": 1,
+ "in_preview": 1,
+ "label": "Image",
+ "print_hide": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "published",
+ "fieldtype": "Check",
+ "label": "Published"
+ },
+ {
+ "default": "0",
+ "depends_on": "has_variants",
+ "fetch_from": "item_code.has_variants",
+ "fieldname": "has_variants",
+ "fieldtype": "Check",
+ "in_standard_filter": 1,
+ "label": "Has Variants",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "depends_on": "variant_of",
+ "fetch_from": "item_code.variant_of",
+ "fieldname": "variant_of",
+ "fieldtype": "Link",
+ "ignore_user_permissions": 1,
+ "in_standard_filter": 1,
+ "label": "Variant Of",
+ "options": "Item",
+ "read_only": 1,
+ "search_index": 1,
+ "set_only_once": 1
+ },
+ {
+ "fetch_from": "item_code.stock_uom",
+ "fieldname": "stock_uom",
+ "fieldtype": "Link",
+ "label": "Stock UOM",
+ "options": "UOM",
+ "read_only": 1
+ },
+ {
+ "depends_on": "brand",
+ "fetch_from": "item_code.brand",
+ "fieldname": "brand",
+ "fieldtype": "Link",
+ "label": "Brand",
+ "options": "Brand"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "advanced_display_section",
+ "fieldtype": "Section Break",
+ "label": "Advanced Display Content"
+ },
+ {
+ "fieldname": "display_section",
+ "fieldtype": "Section Break",
+ "label": "Display Images"
+ },
+ {
+ "fieldname": "column_break_27",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_22",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "item_code.description",
+ "fieldname": "description",
+ "fieldtype": "Text Editor",
+ "label": "Item Description",
+ "read_only": 1
+ },
+ {
+ "default": "WEB-ITM-.####",
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "hidden": 1,
+ "label": "Naming Series",
+ "no_copy": 1,
+ "options": "WEB-ITM-.####",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "display_additional_information_section",
+ "fieldtype": "Section Break",
+ "label": "Display Additional Information"
+ },
+ {
+ "depends_on": "show_tabbed_section",
+ "fieldname": "tabs",
+ "fieldtype": "Table",
+ "label": "Tabs",
+ "options": "Website Item Tabbed Section"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_tabbed_section",
+ "fieldtype": "Check",
+ "label": "Add Section with Tabs"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "offers_section",
+ "fieldtype": "Section Break",
+ "label": "Offers"
+ },
+ {
+ "fieldname": "offers",
+ "fieldtype": "Table",
+ "label": "Offers to Display",
+ "options": "Website Offer"
+ }
+ ],
+ "has_web_view": 1,
+ "image_field": "image",
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-04-22 15:29:13.541145",
+ "modified_by": "Administrator",
+ "module": "E-commerce",
+ "name": "Website Item",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Website Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "search_fields": "item_code, item_name ,item_group",
+ "show_name_in_global_search": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "item_name",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py
new file mode 100644
index 0000000..028ee76
--- /dev/null
+++ b/erpnext/e_commerce/doctype/website_item/website_item.py
@@ -0,0 +1,426 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import json
+import itertools
+from six import string_types
+from frappe import _
+
+from frappe.website.website_generator import WebsiteGenerator
+from frappe.utils import cstr, random_string, cint, flt
+from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow
+
+from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, invalidate_cache_for)
+from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
+
+# SEARCH
+from erpnext.e_commerce.website_item_indexing import (
+ insert_item_to_index,
+ update_index_for_item,
+ delete_item_from_index
+)
+# -----
+
+class WebsiteItem(WebsiteGenerator):
+ website = frappe._dict(
+ page_title_field="web_item_name",
+ condition_field="published",
+ template="templates/generators/item/item.html",
+ no_cache=1
+ )
+
+ def onload(self):
+ super(WebsiteItem, self).onload()
+
+ def validate(self):
+ super(WebsiteItem, self).validate()
+
+ if not self.item_code:
+ frappe.throw(_("Item Code is required"), title=_("Mandatory"))
+
+ self.validate_duplicate_website_item()
+ self.validate_website_image()
+ self.make_thumbnail()
+ self.publish_unpublish_desk_item(publish=True)
+
+ if not self.get("__islocal"):
+ self.old_website_item_groups = frappe.db.sql_list("""select item_group
+ from `tabWebsite Item Group`
+ where parentfield='website_item_groups' and parenttype='Item' and parent=%s""", self.name)
+
+ def on_update(self):
+ invalidate_cache_for_web_item(self)
+ self.update_template_item()
+
+ def on_trash(self):
+ super(WebsiteItem, self).on_trash()
+ # Delete Item from search index
+ delete_item_from_index(self)
+ self.publish_unpublish_desk_item(publish=False)
+
+ def validate_duplicate_website_item(self):
+ existing_web_item = frappe.db.exists("Website Item", {"item_code": self.item_code})
+ if existing_web_item and existing_web_item != self.name:
+ message = _("Website Item already exists against Item {0}").format(frappe.bold(self.item_code))
+ frappe.throw(message, title=_("Already Published"))
+
+ def publish_unpublish_desk_item(self, publish=True):
+ if frappe.db.get_value("Item", self.item_code, "published_in_website") and publish:
+ return # if already published don't publish again
+ frappe.db.set_value("Item", self.item_code, "published_in_website", publish)
+
+ def make_route(self):
+ """Called from set_route in WebsiteGenerator."""
+ if not self.route:
+ return cstr(frappe.db.get_value('Item Group', self.item_group,
+ 'route')) + '/' + self.scrub((self.item_name if self.item_name else self.item_code) + '-' + random_string(5))
+
+ def update_template_item(self):
+ """Set Show in Website for Template Item if True for its Variant"""
+ if self.variant_of:
+ if self.published:
+ # show template
+ template_item = frappe.get_doc("Item", self.variant_of)
+
+ if not template_item.published_in_website:
+ template_item.flags.ignore_permissions = True
+ make_website_item(template_item)
+
+ def validate_website_image(self):
+ if frappe.flags.in_import:
+ return
+
+ """Validate if the website image is a public file"""
+ auto_set_website_image = False
+ if not self.website_image and self.image:
+ auto_set_website_image = True
+ self.website_image = self.image
+
+ if not self.website_image:
+ return
+
+ # find if website image url exists as public
+ file_doc = frappe.get_all("File", filters={
+ "file_url": self.website_image
+ }, fields=["name", "is_private"], order_by="is_private asc", limit_page_length=1)
+
+ if file_doc:
+ file_doc = file_doc[0]
+
+ if not file_doc:
+ if not auto_set_website_image:
+ frappe.msgprint(_("Website Image {0} attached to Item {1} cannot be found").format(self.website_image, self.name))
+
+ self.website_image = None
+
+ elif file_doc.is_private:
+ if not auto_set_website_image:
+ frappe.msgprint(_("Website Image should be a public file or website URL"))
+
+ self.website_image = None
+
+ def make_thumbnail(self):
+ if frappe.flags.in_import:
+ return
+
+ """Make a thumbnail of `website_image`"""
+ import requests.exceptions
+
+ if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"):
+ self.thumbnail = None
+
+ if self.website_image and not self.thumbnail:
+ file_doc = None
+
+ try:
+ file_doc = frappe.get_doc("File", {
+ "file_url": self.website_image,
+ "attached_to_doctype": "Website Item",
+ "attached_to_name": self.name
+ })
+ except frappe.DoesNotExistError:
+ pass
+ # cleanup
+ frappe.local.message_log.pop()
+
+ except requests.exceptions.HTTPError:
+ frappe.msgprint(_("Warning: Invalid attachment {0}").format(self.website_image))
+ self.website_image = None
+
+ except requests.exceptions.SSLError:
+ frappe.msgprint(
+ _("Warning: Invalid SSL certificate on attachment {0}").format(self.website_image))
+ self.website_image = None
+
+ # for CSV import
+ if self.website_image and not file_doc:
+ try:
+ file_doc = frappe.get_doc({
+ "doctype": "File",
+ "file_url": self.website_image,
+ "attached_to_doctype": "Website Item",
+ "attached_to_name": self.name
+ }).save()
+
+ except IOError:
+ self.website_image = None
+
+ if file_doc:
+ if not file_doc.thumbnail_url:
+ file_doc.make_thumbnail()
+
+ self.thumbnail = file_doc.thumbnail_url
+
+ def get_context(self, context):
+ context.show_search = True
+ context.search_link = '/search'
+
+ context.parents = get_parent_item_groups(self.item_group, from_item=True)
+ self.attributes = frappe.get_all("Item Variant Attribute",
+ fields=["attribute", "attribute_value"],
+ filters={"parent": self.item_code})
+ self.set_variant_context(context)
+ self.set_attribute_context(context)
+ self.set_disabled_attributes(context)
+ self.set_metatags(context)
+ self.set_shopping_cart_data(context)
+ self.get_product_details_section(context)
+ get_item_reviews(self.name, 0, 4, context)
+
+ context.wished = False
+ if frappe.db.exists("Wishlist Items", {"item_code": self.item_code, "parent": frappe.session.user}):
+ context.wished = True
+
+ return context
+
+ def set_variant_context(self, context):
+ if self.has_variants:
+ context.no_cache = True
+
+ # load variants
+ # also used in set_attribute_context
+ context.variants = frappe.get_all("Item",
+ filters={"variant_of": self.name, "show_variant_in_website": 1},
+ order_by="name asc")
+
+ variant = frappe.form_dict.variant
+ if not variant and context.variants:
+ # the case when the item is opened for the first time from its list
+ variant = context.variants[0]
+
+ if variant:
+ context.variant = frappe.get_doc("Item", variant)
+
+ for fieldname in ("website_image", "website_image_alt", "web_long_description", "description",
+ "website_specifications"):
+ 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:
+ if context.variant and context.variant.slideshow:
+ context.update(get_slideshow(context.variant))
+ else:
+ context.update(get_slideshow(self))
+
+ def set_attribute_context(self, context):
+ if self.has_variants:
+ attribute_values_available = {}
+ context.attribute_values = {}
+ context.selected_attributes = {}
+
+ # load attributes
+ for v in context.variants:
+ v.attributes = frappe.get_all("Item Variant Attribute",
+ fields=["attribute", "attribute_value"],
+ filters={"parent": v.name})
+ # make a map for easier access in templates
+ v.attribute_map = frappe._dict({})
+ for attr in v.attributes:
+ v.attribute_map[attr.attribute] = attr.attribute_value
+
+ for attr in v.attributes:
+ values = attribute_values_available.setdefault(attr.attribute, [])
+ if attr.attribute_value not in values:
+ values.append(attr.attribute_value)
+
+ if v.name == context.variant.name:
+ context.selected_attributes[attr.attribute] = attr.attribute_value
+
+ # filter attributes, order based on attribute table
+ for attr in self.attributes:
+ values = context.attribute_values.setdefault(attr.attribute, [])
+
+ 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)
+
+ 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 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):
+ context.metatags = frappe._dict({})
+
+ safe_description = frappe.utils.to_markdown(self.description)
+
+ context.metatags.url = frappe.utils.get_url() + '/' + context.route
+
+ if context.website_image:
+ if context.website_image.startswith('http'):
+ url = context.website_image
+ else:
+ url = frappe.utils.get_url() + context.website_image
+ context.metatags.image = url
+
+ context.metatags.description = safe_description[:300]
+
+ context.metatags.title = self.item_name or self.item_code
+
+ context.metatags['og:type'] = 'product'
+ context.metatags['og:site_name'] = 'ERPNext'
+
+ def set_shopping_cart_data(self, context):
+ from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
+ context.shopping_cart = get_product_info_for_website(self.item_code, skip_quotation_creation=True)
+
+ def copy_specification_from_item_group(self):
+ self.set("website_specifications", [])
+ if self.item_group:
+ for label, desc in frappe.db.get_values("Item Website Specification",
+ {"parent": self.item_group}, ["label", "description"]):
+ row = self.append("website_specifications")
+ row.label = label
+ row.description = desc
+
+ def get_product_details_section(self, context):
+ """ Get section with tabs or website specifications. """
+ context.show_tabs = self.show_tabbed_section
+ if self.show_tabbed_section and (self.tabs or self.website_specifications):
+ context.tabs = self.get_tabs()
+ else:
+ context.website_specifications = self.website_specifications
+
+ def get_tabs(self):
+ tab_values = {}
+ tab_values["tab_1_title"] = "Product Details"
+ tab_values["tab_1_content"] = frappe.render_template(
+ "templates/generators/item/item_specifications.html",
+ {
+ "website_specifications": self.website_specifications,
+ "show_tabs": self.show_tabbed_section
+ })
+
+ for row in self.tabs:
+ tab_values[f"tab_{row.idx + 1}_title"] = _(row.label)
+ tab_values[f"tab_{row.idx + 1}_content"] = row.content
+
+ return tab_values
+
+def invalidate_cache_for_web_item(doc):
+ """Invalidate Website Item Group cache and rebuild ItemVariantsCacheManager."""
+ from erpnext.stock.doctype.item.item import invalidate_item_variants_cache_for_website
+
+ invalidate_cache_for(doc, doc.item_group)
+
+ website_item_groups = list(set((doc.get("old_website_item_groups") or [])
+ + [d.item_group for d in doc.get({"doctype": "Website Item Group"}) if d.item_group]))
+
+ for item_group in website_item_groups:
+ invalidate_cache_for(doc, item_group)
+
+ # Update Search Cache
+ update_index_for_item(doc)
+
+ invalidate_item_variants_cache_for_website(doc)
+
+@frappe.whitelist()
+def make_website_item(doc, save=True):
+ if not doc:
+ return
+
+ if isinstance(doc, string_types):
+ doc = json.loads(doc)
+
+ if frappe.db.exists("Website Item", {"item_code": doc.get("item_code")}):
+ message = _("Website Item already exists against {0}").format(frappe.bold(doc.get("item_code")))
+ frappe.throw(message, title=_("Already Published"))
+
+ website_item = frappe.new_doc("Website Item")
+ website_item.web_item_name = doc.get("item_name")
+
+ fields_to_map = ["item_code", "item_name", "item_group", "stock_uom", "brand", "image",
+ "has_variants", "variant_of", "description"]
+ for field in fields_to_map:
+ website_item.update({field: doc.get(field)})
+
+ if not save:
+ return website_item
+
+ website_item.save()
+
+ # Add to search cache
+ insert_item_to_index(website_item)
+
+ return [website_item.name, website_item.web_item_name]
+
+def on_doctype_update():
+ # since route is a Text column, it needs a length for indexing
+ frappe.db.add_index("Website Item", ["route(500)"])
\ No newline at end of file
diff --git a/erpnext/e_commerce/doctype/website_item/website_item_list.js b/erpnext/e_commerce/doctype/website_item/website_item_list.js
new file mode 100644
index 0000000..21be942
--- /dev/null
+++ b/erpnext/e_commerce/doctype/website_item/website_item_list.js
@@ -0,0 +1,20 @@
+frappe.listview_settings['Website Item'] = {
+ add_fields: ["item_name", "web_item_name", "published", "image", "has_variants", "variant_of"],
+ filters: [["published", "=", "1"]],
+
+ get_indicator: function(doc) {
+ if (doc.has_variants && doc.published) {
+ return [__("Template"), "orange", "has_variants,=,Yes|published,=,1"];
+ } else if (doc.has_variants && !doc.published) {
+ return [__("Template"), "grey", "has_variants,=,Yes|published,=,0"];
+ } else if (doc.variant_of && doc.published) {
+ return [__("Variant"), "blue", "published,=,1|variant_of,=," + doc.variant_of];
+ } else if (doc.variant_of && !doc.published) {
+ return [__("Variant"), "grey", "published,=,0|variant_of,=," + doc.variant_of];
+ } else if (doc.published) {
+ return [__("Published"), "green", "published,=,1"];
+ } else {
+ return [__("Not Published"), "grey", "published,=,0"];
+ }
+ }
+};
\ No newline at end of file
diff --git a/erpnext/portal/doctype/products_settings/__init__.py b/erpnext/e_commerce/doctype/website_item_tabbed_section/__init__.py
similarity index 100%
copy from erpnext/portal/doctype/products_settings/__init__.py
copy to erpnext/e_commerce/doctype/website_item_tabbed_section/__init__.py
diff --git a/erpnext/e_commerce/doctype/website_item_tabbed_section/website_item_tabbed_section.json b/erpnext/e_commerce/doctype/website_item_tabbed_section/website_item_tabbed_section.json
new file mode 100644
index 0000000..6601dd8
--- /dev/null
+++ b/erpnext/e_commerce/doctype/website_item_tabbed_section/website_item_tabbed_section.json
@@ -0,0 +1,37 @@
+{
+ "actions": [],
+ "creation": "2021-03-18 20:32:15.321402",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "label",
+ "content"
+ ],
+ "fields": [
+ {
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Label"
+ },
+ {
+ "fieldname": "content",
+ "fieldtype": "HTML Editor",
+ "in_list_view": 1,
+ "label": "Content"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-03-18 20:35:26.991192",
+ "modified_by": "Administrator",
+ "module": "E-commerce",
+ "name": "Website Item Tabbed Section",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/e_commerce/doctype/website_item_tabbed_section/website_item_tabbed_section.py b/erpnext/e_commerce/doctype/website_item_tabbed_section/website_item_tabbed_section.py
new file mode 100644
index 0000000..8459e62
--- /dev/null
+++ b/erpnext/e_commerce/doctype/website_item_tabbed_section/website_item_tabbed_section.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class WebsiteItemTabbedSection(Document):
+ pass
diff --git a/erpnext/portal/doctype/products_settings/__init__.py b/erpnext/e_commerce/doctype/website_offer/__init__.py
similarity index 100%
copy from erpnext/portal/doctype/products_settings/__init__.py
copy to erpnext/e_commerce/doctype/website_offer/__init__.py
diff --git a/erpnext/e_commerce/doctype/website_offer/website_offer.json b/erpnext/e_commerce/doctype/website_offer/website_offer.json
new file mode 100644
index 0000000..627d548
--- /dev/null
+++ b/erpnext/e_commerce/doctype/website_offer/website_offer.json
@@ -0,0 +1,43 @@
+{
+ "actions": [],
+ "creation": "2021-04-21 13:37:14.162162",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "offer_title",
+ "offer_subtitle",
+ "offer_details"
+ ],
+ "fields": [
+ {
+ "fieldname": "offer_title",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Offer Title"
+ },
+ {
+ "fieldname": "offer_subtitle",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Offer Subtitle"
+ },
+ {
+ "fieldname": "offer_details",
+ "fieldtype": "Text Editor",
+ "label": "Offer Details"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-04-21 13:56:04.660331",
+ "modified_by": "Administrator",
+ "module": "E-commerce",
+ "name": "Website Offer",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/e_commerce/doctype/website_offer/website_offer.py b/erpnext/e_commerce/doctype/website_offer/website_offer.py
new file mode 100644
index 0000000..59d580e
--- /dev/null
+++ b/erpnext/e_commerce/doctype/website_offer/website_offer.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.model.document import Document
+
+class WebsiteOffer(Document):
+ pass
+
+@frappe.whitelist()
+def get_offer_details(offer_id):
+ return frappe.db.get_value('Website Offer', {'name': offer_id}, ['offer_details'])
diff --git a/erpnext/portal/product_configurator/__init__.py b/erpnext/e_commerce/doctype/wishlist/__init__.py
similarity index 100%
copy from erpnext/portal/product_configurator/__init__.py
copy to erpnext/e_commerce/doctype/wishlist/__init__.py
diff --git a/erpnext/e_commerce/doctype/wishlist/test_wishlist.py b/erpnext/e_commerce/doctype/wishlist/test_wishlist.py
new file mode 100644
index 0000000..6565e71
--- /dev/null
+++ b/erpnext/e_commerce/doctype/wishlist/test_wishlist.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestWishlist(unittest.TestCase):
+ pass
diff --git a/erpnext/e_commerce/doctype/wishlist/wishlist.js b/erpnext/e_commerce/doctype/wishlist/wishlist.js
new file mode 100644
index 0000000..d96e552
--- /dev/null
+++ b/erpnext/e_commerce/doctype/wishlist/wishlist.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Wishlist', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/e_commerce/doctype/wishlist/wishlist.json b/erpnext/e_commerce/doctype/wishlist/wishlist.json
new file mode 100644
index 0000000..ae24207
--- /dev/null
+++ b/erpnext/e_commerce/doctype/wishlist/wishlist.json
@@ -0,0 +1,65 @@
+{
+ "actions": [],
+ "autoname": "field:user",
+ "creation": "2021-03-10 18:52:28.769126",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "user",
+ "section_break_2",
+ "items"
+ ],
+ "fields": [
+ {
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "User",
+ "options": "User",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "section_break_2",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "label": "Items",
+ "options": "Wishlist Items"
+ }
+ ],
+ "in_create": 1,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-03-24 20:42:58.402031",
+ "modified_by": "Administrator",
+ "module": "E-commerce",
+ "name": "Wishlist",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Website Manager",
+ "share": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/e_commerce/doctype/wishlist/wishlist.py b/erpnext/e_commerce/doctype/wishlist/wishlist.py
new file mode 100644
index 0000000..c817657
--- /dev/null
+++ b/erpnext/e_commerce/doctype/wishlist/wishlist.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.model.document import Document
+
+class Wishlist(Document):
+ pass
+
+@frappe.whitelist()
+def add_to_wishlist(item_code, price, formatted_price=None):
+ """Insert Item into wishlist."""
+
+ if frappe.db.exists("Wishlist Items", {"item_code": item_code, "parent": frappe.session.user}):
+ return
+
+ web_item_data = frappe.db.get_value("Website Item", {"item_code": item_code},
+ ["image", "website_warehouse", "name", "item_name", "item_group", "route"]
+ , as_dict=1)
+
+ wished_item_dict = {
+ "item_code": item_code,
+ "item_name": web_item_data.get("item_name"),
+ "item_group": web_item_data.get("item_group"),
+ "website_item": web_item_data.get("name"),
+ "price": frappe.utils.flt(price),
+ "formatted_price": formatted_price,
+ "image": web_item_data.get("image"),
+ "warehouse": web_item_data.get("website_warehouse"),
+ "route": web_item_data.get("route")
+ }
+
+ if not frappe.db.exists("Wishlist", frappe.session.user):
+ # initialise wishlist
+ wishlist = frappe.get_doc({"doctype": "Wishlist"})
+ wishlist.user = frappe.session.user
+ wishlist.append("items", wished_item_dict)
+ wishlist.save(ignore_permissions=True)
+ else:
+ wishlist = frappe.get_doc("Wishlist", frappe.session.user)
+ item = wishlist.append('items', wished_item_dict)
+ item.db_insert()
+
+ if hasattr(frappe.local, "cookie_manager"):
+ frappe.local.cookie_manager.set_cookie("wish_count", str(len(wishlist.items)))
+
+@frappe.whitelist()
+def remove_from_wishlist(item_code):
+ if frappe.db.exists("Wishlist Items", {"item_code": item_code, "parent": frappe.session.user}):
+ frappe.db.sql("""
+ delete
+ from `tabWishlist Items`
+ where item_code=%(item_code)s
+ """ % {"item_code": frappe.db.escape(item_code)})
+
+ frappe.db.commit()
+
+ wishlist = frappe.get_doc("Wishlist", frappe.session.user)
+ if hasattr(frappe.local, "cookie_manager"):
+ frappe.local.cookie_manager.set_cookie("wish_count", str(len(wishlist.items)))
\ No newline at end of file
diff --git a/erpnext/portal/doctype/products_settings/__init__.py b/erpnext/e_commerce/doctype/wishlist_items/__init__.py
similarity index 100%
copy from erpnext/portal/doctype/products_settings/__init__.py
copy to erpnext/e_commerce/doctype/wishlist_items/__init__.py
diff --git a/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.json b/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.json
new file mode 100644
index 0000000..6623921
--- /dev/null
+++ b/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.json
@@ -0,0 +1,143 @@
+{
+ "actions": [],
+ "creation": "2021-03-10 19:03:00.662714",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "item_code",
+ "website_item",
+ "column_break_3",
+ "item_name",
+ "item_group",
+ "item_details_section",
+ "description",
+ "column_break_7",
+ "route",
+ "image",
+ "image_view",
+ "section_break_8",
+ "price",
+ "formatted_price",
+ "warehouse_section",
+ "warehouse"
+ ],
+ "fields": [
+ {
+ "fetch_from": "website_item.item_code",
+ "fetch_if_empty": 1,
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item Code",
+ "options": "Item",
+ "reqd": 1
+ },
+ {
+ "fieldname": "website_item",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Website Item",
+ "options": "Website Item"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "item_code.item_name",
+ "fetch_if_empty": 1,
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "item_details_section",
+ "fieldtype": "Section Break",
+ "label": "Item Details"
+ },
+ {
+ "fetch_from": "item_code.description",
+ "fetch_if_empty": 1,
+ "fieldname": "description",
+ "fieldtype": "Text Editor",
+ "label": "Description"
+ },
+ {
+ "fieldname": "column_break_7",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "item_code.image",
+ "fetch_if_empty": 1,
+ "fieldname": "image",
+ "fieldtype": "Attach",
+ "hidden": 1,
+ "label": "Image"
+ },
+ {
+ "fetch_from": "item_code.image",
+ "fetch_if_empty": 1,
+ "fieldname": "image_view",
+ "fieldtype": "Image",
+ "hidden": 1,
+ "label": "Image View",
+ "options": "image",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "warehouse_section",
+ "fieldtype": "Section Break",
+ "label": "Warehouse"
+ },
+ {
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "section_break_8",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "price",
+ "fieldtype": "Float",
+ "label": "Price"
+ },
+ {
+ "fetch_from": "item_code.item_group",
+ "fetch_if_empty": 1,
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group"
+ },
+ {
+ "fetch_from": "website_item.route",
+ "fetch_if_empty": 1,
+ "fieldname": "route",
+ "fieldtype": "Small Text",
+ "label": "Route"
+ },
+ {
+ "fieldname": "formatted_price",
+ "fieldtype": "Data",
+ "label": "Formatted Price"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-03-18 16:04:52.965613",
+ "modified_by": "Administrator",
+ "module": "E-commerce",
+ "name": "Wishlist Items",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.py b/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.py
new file mode 100644
index 0000000..25ce17d
--- /dev/null
+++ b/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class WishlistItems(Document):
+ pass
diff --git a/erpnext/shopping_cart/filters.py b/erpnext/e_commerce/filters.py
similarity index 64%
rename from erpnext/shopping_cart/filters.py
rename to erpnext/e_commerce/filters.py
index 4787ae5..0d96a11 100644
--- a/erpnext/shopping_cart/filters.py
+++ b/erpnext/e_commerce/filters.py
@@ -1,21 +1,23 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
-
import frappe
-
+from frappe import _dict
+from frappe.utils import floor
class ProductFiltersBuilder:
def __init__(self, item_group=None):
- if not item_group or item_group == "Products Settings":
- self.doc = frappe.get_doc("Products Settings")
+ if not item_group or item_group == "E Commerce Settings":
+ self.doc = frappe.get_doc("E Commerce Settings")
else:
self.doc = frappe.get_doc("Item Group", item_group)
self.item_group = item_group
def get_field_filters(self):
+ if not self.item_group and not self.doc.enable_field_filters:
+ return
+
filter_fields = [row.fieldname for row in self.doc.filter_fields]
meta = frappe.get_meta('Item')
@@ -31,7 +33,7 @@
["Website Item Group", "item_group", "=", self.item_group]
])
- values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname)
+ values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, distinct="True", pluck=df.fieldname)
else:
doctype = df.get_link_doctype()
@@ -57,6 +59,9 @@
return filter_data
def get_attribute_filters(self):
+ if not self.item_group and not self.doc.enable_attribute_filters:
+ return
+
attributes = [row.attribute for row in self.doc.filter_attributes]
if not attributes:
@@ -84,3 +89,19 @@
for name, values in attribute_value_map.items():
out.append(frappe._dict(name=name, item_attribute_values=values))
return out
+
+ def get_discount_filters(self, discounts):
+ discount_filters = []
+
+ # [25.89, 60.5]
+ min_discount, max_discount = discounts[0], discounts[1]
+ # [25, 60]
+ min_range_absolute, max_range_absolute = floor(min_discount), floor(max_discount)
+ min_range = int(min_discount - (min_range_absolute % 10)) # 20
+ max_range = int(max_discount - (max_range_absolute % 10)) # 60
+
+ for discount in range(min_range, (max_range + 1), 10):
+ label = f"{discount}% and above"
+ discount_filters.append([discount, label])
+
+ return discount_filters
diff --git a/erpnext/portal/product_configurator/__init__.py b/erpnext/e_commerce/product_configurator/__init__.py
similarity index 100%
rename from erpnext/portal/product_configurator/__init__.py
rename to erpnext/e_commerce/product_configurator/__init__.py
diff --git a/erpnext/portal/product_configurator/item_variants_cache.py b/erpnext/e_commerce/product_configurator/item_variants_cache.py
similarity index 100%
rename from erpnext/portal/product_configurator/item_variants_cache.py
rename to erpnext/e_commerce/product_configurator/item_variants_cache.py
diff --git a/erpnext/e_commerce/product_configurator/test_product_configurator.py b/erpnext/e_commerce/product_configurator/test_product_configurator.py
new file mode 100644
index 0000000..abc1f30
--- /dev/null
+++ b/erpnext/e_commerce/product_configurator/test_product_configurator.py
@@ -0,0 +1,118 @@
+import unittest
+
+import frappe
+from bs4 import BeautifulSoup
+from frappe.utils import get_html_for_route
+
+from erpnext.e_commerce.product_query import ProductQuery
+from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
+
+test_dependencies = ["Item"]
+
+class TestProductConfigurator(unittest.TestCase):
+ def setUp(self):
+ self.create_variant_item()
+ self.publish_items_on_website()
+
+ def test_product_list(self):
+ usual_items = frappe.get_all('Website Item', {'published': 1, 'has_variants': 0, 'variant_of': ['is', 'not set']})
+ template_items = frappe.get_all('Website Item', {'published': 1, 'has_variants': 1})
+ variant_items = frappe.get_all('Website Item', {'published': 1, 'variant_of': ['is', 'set']})
+
+ 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 + usual_items))
+
+ items_with_item_group = frappe.get_all('Website Item', {'item_group': '_Test Item Group Desktops', 'published': 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))
+
+
+ def test_get_products_for_website(self):
+ engine = ProductQuery()
+ items = engine.query(attributes={
+ 'Test Size': ['Medium']
+ })
+ 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
+
+ def create_variant_item(self):
+ if not frappe.db.exists('Item', '_Test Variant Item 1'):
+ frappe.get_doc({
+ "description": "_Test Variant Item 12",
+ "doctype": "Item",
+ "is_stock_item": 1,
+ "variant_of": "_Test Variant Item",
+ "item_code": "_Test Variant Item 1",
+ "item_group": "_Test Item Group",
+ "item_name": "_Test Variant Item 1",
+ "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": "Medium"
+ }
+ ]
+ }).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]
+
+ def publish_items_on_website(self):
+ if frappe.db.exists("Item", "_Test Item") and not frappe.db.exists("Website Item", {"item_code": "_Test Item"}):
+ make_website_item(frappe.get_cached_doc("Item", "_Test Item"))
+
+ if frappe.db.exists("Item", "_Test Variant Item") and not frappe.db.exists("Website Item", {"item_code": "_Test Variant Item"}):
+ make_website_item(frappe.get_cached_doc("Item", "_Test Variant Item"))
+
+ make_website_item(frappe.get_cached_doc("Item", "_Test Variant Item 1"))
+
+ # teardown
+ doc.delete()
+ item_group_doc.delete()
diff --git a/erpnext/e_commerce/product_configurator/utils.py b/erpnext/e_commerce/product_configurator/utils.py
new file mode 100644
index 0000000..9faaa5d
--- /dev/null
+++ b/erpnext/e_commerce/product_configurator/utils.py
@@ -0,0 +1,196 @@
+import frappe
+from frappe.utils import cint
+
+from erpnext.e_commerce.product_configurator.item_variants_cache import ItemVariantsCacheManager
+
+def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
+ items = []
+
+ for attribute, values in attribute_filters.items():
+ attribute_values = values
+
+ if not isinstance(attribute_values, list):
+ attribute_values = [attribute_values]
+
+ if not attribute_values:
+ continue
+
+ wheres = []
+ query_values = []
+ for attribute_value in attribute_values:
+ wheres.append('( attribute = %s and attribute_value = %s )')
+ query_values += [attribute, attribute_value]
+
+ attribute_query = ' or '.join(wheres)
+
+ if template_item_code:
+ variant_of_query = 'AND t2.variant_of = %s'
+ query_values.append(template_item_code)
+ else:
+ variant_of_query = ''
+
+ query = '''
+ SELECT
+ t1.parent
+ FROM
+ `tabItem Variant Attribute` t1
+ WHERE
+ 1 = 1
+ AND (
+ {attribute_query}
+ )
+ AND EXISTS (
+ SELECT
+ 1
+ FROM
+ `tabItem` t2
+ WHERE
+ t2.name = t1.parent
+ {variant_of_query}
+ )
+ GROUP BY
+ t1.parent
+ ORDER BY
+ NULL
+ '''.format(attribute_query=attribute_query, variant_of_query=variant_of_query)
+
+ item_codes = set([r[0] for r in frappe.db.sql(query, query_values)])
+ items.append(item_codes)
+
+ res = list(set.intersection(*items))
+
+ return res
+
+@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.e_commerce.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
+
diff --git a/erpnext/e_commerce/product_query.py b/erpnext/e_commerce/product_query.py
new file mode 100644
index 0000000..c186a05
--- /dev/null
+++ b/erpnext/e_commerce/product_query.py
@@ -0,0 +1,206 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+
+from frappe.utils import flt
+
+from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
+
+
+class ProductQuery:
+ """Query engine for product listing
+
+ Attributes:
+ fields (list): Fields to fetch in query
+ conditions (string): Conditions for query building
+ or_conditions (string): Search conditions
+ page_length (Int): Length of page for the query
+ settings (Document): E Commerce Settings DocType
+ """
+
+ def __init__(self):
+ self.settings = frappe.get_doc("E Commerce Settings")
+ self.page_length = self.settings.products_per_page or 20
+ self.fields = ['wi.name', 'wi.item_name', 'wi.item_code', 'wi.website_image', 'wi.variant_of',
+ 'wi.has_variants', 'wi.item_group', 'wi.image', 'wi.web_long_description', 'wi.description',
+ 'wi.route', 'wi.website_warehouse']
+ self.conditions = ""
+ self.or_conditions = ""
+ self.substitutions = []
+
+ def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None):
+ """Summary
+
+ Args:
+ attributes (dict, optional): Item Attribute filters
+ fields (dict, optional): Field level filters
+ search_term (str, optional): Search term to lookup
+ start (int, optional): Page start
+
+ Returns:
+ list: List of results with set fields
+ """
+ result, discount_list = [], []
+
+ if fields:
+ self.build_fields_filters(fields)
+ if search_term:
+ self.build_search_filters(search_term)
+ if self.settings.hide_variants:
+ self.conditions += " and wi.variant_of is null"
+
+ if attributes:
+ result = self.query_items_with_attributes(attributes, start)
+ else:
+ result = self.query_items(self.conditions, self.or_conditions,
+ self.substitutions, start=start)
+
+ # add price and availability info in results
+ for item in result:
+ product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
+
+ if product_info and product_info['price']:
+ self.get_price_discount_info(item, product_info['price'], discount_list)
+
+ if self.settings.show_stock_availability:
+ self.get_stock_availability(item)
+
+ item.wished = False
+ if frappe.db.exists("Wishlist Items", {"item_code": item.item_code, "parent": frappe.session.user}):
+ item.wished = True
+
+ discounts = []
+ if discount_list:
+ discounts = [min(discount_list), max(discount_list)]
+
+ if fields and "discount" in fields:
+ discount_percent = frappe.utils.flt(fields["discount"][0])
+ result = [row for row in result if row.get("discount_percent") and row.discount_percent >= discount_percent]
+
+ return result, discounts
+
+ def get_price_discount_info(self, item, price_object, discount_list):
+ """Modify item object and add price details."""
+ item.formatted_mrp = price_object.get('formatted_mrp')
+ item.formatted_price = price_object.get('formatted_price')
+
+ if price_object.get('discount_percent'):
+ item.discount_percent = flt(price_object.discount_percent)
+ discount_list.append(price_object.discount_percent)
+
+ if item.formatted_mrp:
+ item.discount = price_object.get('formatted_discount_percent') or \
+ price_object.get('formatted_discount_rate')
+ item.price = price_object.get('price_list_rate')
+
+ def get_stock_availability(self, item):
+ """Modify item object and add stock details."""
+ if item.get("website_warehouse"):
+ stock_qty = frappe.utils.flt(
+ frappe.db.get_value("Bin", {"item_code": item.item_code, "warehouse": item.get("website_warehouse")},
+ "actual_qty"))
+ item.in_stock = "green" if stock_qty else "red"
+ elif not frappe.db.get_value("Item", item.item_code, "is_stock_item"):
+ item.in_stock = "green" # non-stock item will always be available
+
+ def query_items(self, conditions, or_conditions, substitutions, start=0):
+ """Build a query to fetch Website Items based on field filters."""
+ self.query_fields = (", ").join(self.fields)
+
+ return frappe.db.sql("""
+ select distinct {query_fields}
+ from
+ `tabWebsite Item` wi, `tabItem Variant Attribute` iva
+ where
+ wi.published = 1
+ {conditions}
+ {or_conditions}
+ limit {limit} offset {start}
+ """.format(
+ query_fields=self.query_fields,
+ conditions=conditions,
+ or_conditions=or_conditions,
+ limit=self.page_length,
+ start=start),
+ tuple(substitutions),
+ as_dict=1)
+
+ def query_items_with_attributes(self, attributes, start=0):
+ """Build a query to fetch Website Items based on field & attribute filters."""
+ all_items = []
+ self.conditions += " and iva.parent = wi.item_code"
+
+ for attribute, values in attributes.items():
+ if not isinstance(values, list):
+ values = [values]
+
+ conditions_copy = self.conditions
+ substitutions_copy = self.substitutions.copy()
+
+ conditions_copy += " and iva.attribute = '{0}' and iva.attribute_value in ({1})" \
+ .format(attribute, (", ").join(['%s'] * len(values)))
+ substitutions_copy.extend(values)
+
+ items = self.query_items(conditions_copy, self.or_conditions, substitutions_copy, start=start)
+
+ items_dict = {item.name: item for item in items}
+ # TODO: Replace Variants by their parent templates
+
+ all_items.append(set(items_dict.keys()))
+
+ result = [items_dict.get(item) for item in list(set.intersection(*all_items))]
+ return result
+
+ def build_fields_filters(self, filters):
+ """Build filters for field values
+
+ Args:
+ filters (dict): Filters
+ """
+ for field, values in filters.items():
+ if not values or field == "discount":
+ continue
+
+ # handle multiselect fields in filter addition
+ meta = frappe.get_meta('Item', cached=True)
+ df = meta.get_field(field)
+ if df.fieldtype == 'Table MultiSelect':
+ child_doctype = df.options
+ child_meta = frappe.get_meta(child_doctype, cached=True)
+ fields = child_meta.get("fields")
+ if fields:
+ self.filters.append([child_doctype, fields[0].fieldname, 'IN', values])
+ elif isinstance(values, list):
+ # If value is a list use `IN` query
+ self.conditions += " and wi.{0} in ({1})".format(field, (', ').join(['%s'] * len(values)))
+ self.substitutions.extend(values)
+ else:
+ # `=` will be faster than `IN` for most cases
+ self.conditions += " and wi.{0} = '{1}'".format(field, values)
+
+ def build_search_filters(self, search_term):
+ """Query search term in specified fields
+
+ Args:
+ search_term (str): Search candidate
+ """
+ # Default fields to search from
+ default_fields = {'name', 'item_name', 'description', 'item_group'}
+
+ # Get meta search fields
+ meta = frappe.get_meta("Item")
+ meta_fields = set(meta.get_search_fields())
+
+ # Join the meta fields and default fields set
+ search_fields = default_fields.union(meta_fields)
+ try:
+ if frappe.db.count('Item', cache=True) > 50000:
+ search_fields.remove('description')
+ except KeyError:
+ pass
+
+ # Build or filters for query
+ search = '%{}%'.format(search_term)
+ for field in search_fields:
+ self.or_conditions += " or {0} like '{1}'".format(field, search)
diff --git a/erpnext/portal/product_configurator/__init__.py b/erpnext/e_commerce/shopping_cart/__init__.py
similarity index 100%
copy from erpnext/portal/product_configurator/__init__.py
copy to erpnext/e_commerce/shopping_cart/__init__.py
diff --git a/erpnext/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py
similarity index 94%
rename from erpnext/shopping_cart/cart.py
rename to erpnext/e_commerce/shopping_cart/cart.py
index 3f1dfde..7abfb42 100644
--- a/erpnext/shopping_cart/cart.py
+++ b/erpnext/e_commerce/shopping_cart/cart.py
@@ -1,8 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
-
import frappe
import frappe.defaults
from frappe import _, throw
@@ -11,18 +9,16 @@
from frappe.utils import cint, cstr, flt, get_fullname
from frappe.utils.nestedset import get_root_of
+from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import get_shopping_cart_settings
from erpnext.accounts.utils import get_account_name
-from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
- get_shopping_cart_settings,
-)
-from erpnext.utilities.product import get_qty_in_stock
+from erpnext.utilities.product import get_web_item_qty_in_stock
class WebsitePriceListMissingError(frappe.ValidationError):
pass
def set_cart_count(quotation=None):
- if cint(frappe.db.get_singles_value("Shopping Cart Settings", "enabled")):
+ if cint(frappe.db.get_singles_value("E Commerce Settings", "enabled")):
if not quotation:
quotation = _get_cart_quotation()
cart_count = cstr(len(quotation.get("items")))
@@ -49,7 +45,7 @@
"shipping_addresses": get_shipping_addresses(party),
"billing_addresses": get_billing_addresses(party),
"shipping_rules": get_applicable_shipping_rules(party),
- "cart_settings": frappe.get_cached_doc("Shopping Cart Settings")
+ "cart_settings": frappe.get_cached_doc("E Commerce Settings")
}
@frappe.whitelist()
@@ -73,7 +69,7 @@
@frappe.whitelist()
def place_order():
quotation = _get_cart_quotation()
- cart_settings = frappe.db.get_value("Shopping Cart Settings", None,
+ cart_settings = frappe.db.get_value("E Commerce Settings", None,
["company", "allow_items_not_in_stock"], as_dict=1)
quotation.company = cart_settings.company
@@ -97,7 +93,7 @@
item.item_code, ["website_warehouse", "is_stock_item"])
if is_stock_item:
- item_stock = get_qty_in_stock(item.item_code, "website_warehouse")
+ item_stock = get_web_item_qty_in_stock(item.item_code, "website_warehouse")
if not cint(item_stock.in_stock):
throw(_("{1} Not in Stock").format(item.item_code))
if item.qty > item_stock.stock_qty[0][0]:
@@ -157,9 +153,8 @@
set_cart_count(quotation)
- context = get_cart_quotation(quotation)
-
if cint(with_items):
+ context = get_cart_quotation(quotation)
return {
"items": frappe.render_template("templates/includes/cart/cart_items.html",
context),
@@ -168,8 +163,7 @@
}
else:
return {
- 'name': quotation.name,
- 'shopping_cart_menu': get_shopping_cart_menu(context)
+ 'name': quotation.name
}
@frappe.whitelist()
@@ -263,12 +257,12 @@
territory = frappe.db.get_value("Territory", geoip_country)
return territory or \
- frappe.db.get_value("Shopping Cart Settings", None, "territory") or \
+ frappe.db.get_value("E Commerce Settings", None, "territory") or \
get_root_of("Territory")
def decorate_quotation_doc(doc):
for d in doc.get("items", []):
- d.update(frappe.db.get_value("Item", d.item_code,
+ d.update(frappe.db.get_value("Website Item", {"item_code": d.item_code},
["thumbnail", "website_image", "description", "route"], as_dict=True))
return doc
@@ -286,7 +280,7 @@
if quotation:
qdoc = frappe.get_doc("Quotation", quotation[0].name)
else:
- company = frappe.db.get_value("Shopping Cart Settings", None, ["company"])
+ company = frappe.db.get_value("E Commerce Settings", None, ["company"])
qdoc = frappe.get_doc({
"doctype": "Quotation",
"naming_series": get_shopping_cart_settings().quotation_series or "QTN-CART-",
@@ -341,7 +335,7 @@
if not quotation:
quotation = _get_cart_quotation(party)
- cart_settings = frappe.get_doc("Shopping Cart Settings")
+ cart_settings = frappe.get_doc("E Commerce Settings")
set_price_list_and_rate(quotation, cart_settings)
@@ -418,7 +412,7 @@
party_doctype = contact.links[0].link_doctype
party = contact.links[0].link_name
- cart_settings = frappe.get_doc("Shopping Cart Settings")
+ cart_settings = frappe.get_doc("E Commerce Settings")
debtors_account = ''
diff --git a/erpnext/shopping_cart/product_info.py b/erpnext/e_commerce/shopping_cart/product_info.py
similarity index 81%
rename from erpnext/shopping_cart/product_info.py
rename to erpnext/e_commerce/shopping_cart/product_info.py
index fa68636..cd47174 100644
--- a/erpnext/shopping_cart/product_info.py
+++ b/erpnext/e_commerce/shopping_cart/product_info.py
@@ -1,17 +1,14 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
-
import frappe
-from erpnext.shopping_cart.cart import _get_cart_quotation, _set_price_list
-from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
+from erpnext.e_commerce.shopping_cart.cart import _get_cart_quotation, _set_price_list
+from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
get_shopping_cart_settings,
- show_quantity_in_website,
+ show_quantity_in_website
)
-from erpnext.utilities.product import get_non_stock_item_status, get_price, get_qty_in_stock
-
+from erpnext.utilities.product import get_price, get_web_item_qty_in_stock, get_non_stock_item_status
@frappe.whitelist(allow_guest=True)
def get_product_info_for_website(item_code, skip_quotation_creation=False):
@@ -33,8 +30,7 @@
cart_settings.default_customer_group,
cart_settings.company
)
-
- stock_status = get_qty_in_stock(item_code, "website_warehouse")
+ stock_status = get_web_item_qty_in_stock(item_code, "website_warehouse")
product_info = {
"price": price,
diff --git a/erpnext/shopping_cart/search.py b/erpnext/e_commerce/shopping_cart/search.py
similarity index 96%
rename from erpnext/shopping_cart/search.py
rename to erpnext/e_commerce/shopping_cart/search.py
index 5d2de78..30656be 100644
--- a/erpnext/shopping_cart/search.py
+++ b/erpnext/e_commerce/shopping_cart/search.py
@@ -111,7 +111,7 @@
)
def get_all_published_items():
- return frappe.get_all("Item", filters={"variant_of": "", "show_in_website": 1},pluck="name")
+ return frappe.get_all("Website Item", filters={"variant_of": "", "published": 1}, pluck="item_code")
def update_index_for_path(path):
search = ProductSearch(INDEX_NAME)
diff --git a/erpnext/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
similarity index 91%
rename from erpnext/shopping_cart/test_shopping_cart.py
rename to erpnext/e_commerce/shopping_cart/test_shopping_cart.py
index d1284cd..304dab4 100644
--- a/erpnext/shopping_cart/test_shopping_cart.py
+++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
@@ -9,7 +9,8 @@
from frappe.utils import add_months, nowdate
from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule
-from erpnext.shopping_cart.cart import _get_cart_quotation, get_party, update_cart
+from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
+from erpnext.e_commerce.shopping_cart.cart import _get_cart_quotation, get_party, update_cart
from erpnext.tests.utils import create_test_contact_and_address
# test_dependencies = ['Payment Terms Template']
@@ -28,6 +29,11 @@
frappe.set_user("Administrator")
create_test_contact_and_address()
self.enable_shopping_cart()
+ if not frappe.db.exists("Website Item", {"item_code": "_Test Item"}):
+ make_website_item(frappe.get_cached_doc("Item", "_Test Item"))
+
+ if not frappe.db.exists("Website Item", {"item_code": "_Test Item 2"}):
+ make_website_item(frappe.get_cached_doc("Item", "_Test Item 2"))
def tearDown(self):
frappe.set_user("Administrator")
@@ -167,7 +173,7 @@
# helper functions
def enable_shopping_cart(self):
- settings = frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings")
+ settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
settings.update({
"enabled": 1,
@@ -197,7 +203,7 @@
frappe.local.shopping_cart_settings = None
def disable_shopping_cart(self):
- settings = frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings")
+ settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
settings.enabled = 0
settings.save()
frappe.local.shopping_cart_settings = None
diff --git a/erpnext/shopping_cart/utils.py b/erpnext/e_commerce/shopping_cart/utils.py
similarity index 78%
rename from erpnext/shopping_cart/utils.py
rename to erpnext/e_commerce/shopping_cart/utils.py
index f412e61..ce8e560 100644
--- a/erpnext/shopping_cart/utils.py
+++ b/erpnext/e_commerce/shopping_cart/utils.py
@@ -1,14 +1,9 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
-
import frappe
-from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
- is_cart_enabled,
-)
-
+from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import is_cart_enabled
def show_cart_count():
if (is_cart_enabled() and
@@ -21,7 +16,7 @@
role, parties = check_customer_or_supplier()
if role == 'Supplier': return
if show_cart_count():
- from erpnext.shopping_cart.cart import set_cart_count
+ from erpnext.e_commerce.shopping_cart.cart import set_cart_count
set_cart_count()
def clear_cart_count(login_manager):
diff --git a/erpnext/portal/product_configurator/__init__.py b/erpnext/e_commerce/web_template/__init__.py
similarity index 100%
copy from erpnext/portal/product_configurator/__init__.py
copy to erpnext/e_commerce/web_template/__init__.py
diff --git a/erpnext/shopping_cart/web_template/hero_slider/__init__.py b/erpnext/e_commerce/web_template/hero_slider/__init__.py
similarity index 100%
rename from erpnext/shopping_cart/web_template/hero_slider/__init__.py
rename to erpnext/e_commerce/web_template/hero_slider/__init__.py
diff --git a/erpnext/shopping_cart/web_template/hero_slider/hero_slider.html b/erpnext/e_commerce/web_template/hero_slider/hero_slider.html
similarity index 100%
rename from erpnext/shopping_cart/web_template/hero_slider/hero_slider.html
rename to erpnext/e_commerce/web_template/hero_slider/hero_slider.html
diff --git a/erpnext/shopping_cart/web_template/hero_slider/hero_slider.json b/erpnext/e_commerce/web_template/hero_slider/hero_slider.json
similarity index 98%
rename from erpnext/shopping_cart/web_template/hero_slider/hero_slider.json
rename to erpnext/e_commerce/web_template/hero_slider/hero_slider.json
index 04fb1d2..2b1807c 100644
--- a/erpnext/shopping_cart/web_template/hero_slider/hero_slider.json
+++ b/erpnext/e_commerce/web_template/hero_slider/hero_slider.json
@@ -1,4 +1,5 @@
{
+ "__unsaved": 1,
"creation": "2020-11-17 15:21:51.207221",
"docstatus": 0,
"doctype": "Web Template",
@@ -273,9 +274,9 @@
}
],
"idx": 2,
- "modified": "2020-12-29 12:30:02.794994",
+ "modified": "2021-02-24 15:57:05.889709",
"modified_by": "Administrator",
- "module": "Shopping Cart",
+ "module": "E-commerce",
"name": "Hero Slider",
"owner": "Administrator",
"standard": 1,
diff --git a/erpnext/shopping_cart/web_template/item_card_group/__init__.py b/erpnext/e_commerce/web_template/item_card_group/__init__.py
similarity index 100%
rename from erpnext/shopping_cart/web_template/item_card_group/__init__.py
rename to erpnext/e_commerce/web_template/item_card_group/__init__.py
diff --git a/erpnext/shopping_cart/web_template/item_card_group/item_card_group.html b/erpnext/e_commerce/web_template/item_card_group/item_card_group.html
similarity index 86%
rename from erpnext/shopping_cart/web_template/item_card_group/item_card_group.html
rename to erpnext/e_commerce/web_template/item_card_group/item_card_group.html
index fe061d5..33d7bcc 100644
--- a/erpnext/shopping_cart/web_template/item_card_group/item_card_group.html
+++ b/erpnext/e_commerce/web_template/item_card_group/item_card_group.html
@@ -25,9 +25,8 @@
{%- if item -%}
{%- set item = frappe.get_doc("Item", item) -%}
{{ item_card(
- item.item_name, item.image, item.route, item.description,
- None, item.item_group, values['card_' + index + '_featured'],
- True, "Center"
+ item, is_featured=values['card_' + index + '_featured'],
+ is_full_width=True, align="Center"
) }}
{%- endif -%}
{%- endfor -%}
diff --git a/erpnext/shopping_cart/web_template/item_card_group/item_card_group.json b/erpnext/e_commerce/web_template/item_card_group/item_card_group.json
similarity index 97%
rename from erpnext/shopping_cart/web_template/item_card_group/item_card_group.json
rename to erpnext/e_commerce/web_template/item_card_group/item_card_group.json
index ad087b0..724c437 100644
--- a/erpnext/shopping_cart/web_template/item_card_group/item_card_group.json
+++ b/erpnext/e_commerce/web_template/item_card_group/item_card_group.json
@@ -17,15 +17,12 @@
"reqd": 0
},
{
- "__unsaved": 1,
"fieldname": "primary_action_label",
"fieldtype": "Data",
"label": "Primary Action Label",
"reqd": 0
},
{
- "__islocal": 1,
- "__unsaved": 1,
"fieldname": "primary_action",
"fieldtype": "Data",
"label": "Primary Action",
@@ -262,9 +259,9 @@
}
],
"idx": 0,
- "modified": "2020-11-19 18:48:52.633045",
+ "modified": "2021-02-24 16:05:31.242342",
"modified_by": "Administrator",
- "module": "Shopping Cart",
+ "module": "E-commerce",
"name": "Item Card Group",
"owner": "Administrator",
"standard": 1,
diff --git a/erpnext/shopping_cart/web_template/product_card/__init__.py b/erpnext/e_commerce/web_template/product_card/__init__.py
similarity index 100%
rename from erpnext/shopping_cart/web_template/product_card/__init__.py
rename to erpnext/e_commerce/web_template/product_card/__init__.py
diff --git a/erpnext/shopping_cart/web_template/product_card/product_card.html b/erpnext/e_commerce/web_template/product_card/product_card.html
similarity index 100%
rename from erpnext/shopping_cart/web_template/product_card/product_card.html
rename to erpnext/e_commerce/web_template/product_card/product_card.html
diff --git a/erpnext/shopping_cart/web_template/product_card/product_card.json b/erpnext/e_commerce/web_template/product_card/product_card.json
similarity index 82%
rename from erpnext/shopping_cart/web_template/product_card/product_card.json
rename to erpnext/e_commerce/web_template/product_card/product_card.json
index 1059c1b..2eb7374 100644
--- a/erpnext/shopping_cart/web_template/product_card/product_card.json
+++ b/erpnext/e_commerce/web_template/product_card/product_card.json
@@ -5,7 +5,6 @@
"doctype": "Web Template",
"fields": [
{
- "__unsaved": 1,
"fieldname": "item",
"fieldtype": "Link",
"label": "Item",
@@ -13,7 +12,6 @@
"reqd": 0
},
{
- "__unsaved": 1,
"fieldname": "featured",
"fieldtype": "Check",
"label": "Featured",
@@ -22,9 +20,9 @@
}
],
"idx": 0,
- "modified": "2020-11-17 15:33:34.982515",
+ "modified": "2021-02-24 16:05:17.926610",
"modified_by": "Administrator",
- "module": "Shopping Cart",
+ "module": "E-commerce",
"name": "Product Card",
"owner": "Administrator",
"standard": 1,
diff --git a/erpnext/shopping_cart/web_template/product_category_cards/__init__.py b/erpnext/e_commerce/web_template/product_category_cards/__init__.py
similarity index 100%
rename from erpnext/shopping_cart/web_template/product_category_cards/__init__.py
rename to erpnext/e_commerce/web_template/product_category_cards/__init__.py
diff --git a/erpnext/shopping_cart/web_template/product_category_cards/product_category_cards.html b/erpnext/e_commerce/web_template/product_category_cards/product_category_cards.html
similarity index 100%
rename from erpnext/shopping_cart/web_template/product_category_cards/product_category_cards.html
rename to erpnext/e_commerce/web_template/product_category_cards/product_category_cards.html
diff --git a/erpnext/shopping_cart/web_template/product_category_cards/product_category_cards.json b/erpnext/e_commerce/web_template/product_category_cards/product_category_cards.json
similarity index 95%
rename from erpnext/shopping_cart/web_template/product_category_cards/product_category_cards.json
rename to erpnext/e_commerce/web_template/product_category_cards/product_category_cards.json
index ba5f63b..0202165 100644
--- a/erpnext/shopping_cart/web_template/product_category_cards/product_category_cards.json
+++ b/erpnext/e_commerce/web_template/product_category_cards/product_category_cards.json
@@ -74,9 +74,9 @@
}
],
"idx": 0,
- "modified": "2020-11-18 17:26:28.726260",
+ "modified": "2021-02-24 16:03:33.835635",
"modified_by": "Administrator",
- "module": "Shopping Cart",
+ "module": "E-commerce",
"name": "Product Category Cards",
"owner": "Administrator",
"standard": 1,
diff --git a/erpnext/e_commerce/website_item_indexing.py b/erpnext/e_commerce/website_item_indexing.py
new file mode 100644
index 0000000..134939c
--- /dev/null
+++ b/erpnext/e_commerce/website_item_indexing.py
@@ -0,0 +1,186 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+from frappe.utils.redis_wrapper import RedisWrapper
+
+from redisearch import (
+ Client, AutoCompleter,
+ Suggestion, IndexDefinition,
+ TextField, TagField
+ )
+
+def make_key(key):
+ return "{0}|{1}".format(frappe.conf.db_name, key).encode('utf-8')
+
+# GLOBAL CONSTANTS
+WEBSITE_ITEM_INDEX = 'website_items_index'
+WEBSITE_ITEM_KEY_PREFIX = 'website_item:'
+WEBSITE_ITEM_NAME_AUTOCOMPLETE = 'website_items_name_dict'
+WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE = 'website_items_category_dict'
+
+ALLOWED_INDEXABLE_FIELDS_SET = {
+ 'item_code',
+ 'item_name',
+ 'item_group',
+ 'brand',
+ 'description',
+ 'web_long_description'
+}
+
+def create_website_items_index():
+ '''Creates Index Definition'''
+ # CREATE index
+ client = Client(make_key(WEBSITE_ITEM_INDEX), conn=frappe.cache())
+
+ # DROP if already exists
+ try:
+ client.drop_index()
+ except:
+ pass
+
+
+ idx_def = IndexDefinition([make_key(WEBSITE_ITEM_KEY_PREFIX)])
+
+ # Based on e-commerce settings
+ idx_fields = frappe.db.get_single_value(
+ 'E Commerce Settings',
+ 'search_index_fields'
+ ).split(',')
+
+ if 'web_item_name' in idx_fields:
+ idx_fields.remove('web_item_name')
+
+ idx_fields = list(map(to_search_field, idx_fields))
+
+ client.create_index(
+ [TextField("web_item_name", sortable=True)] + idx_fields,
+ definition=idx_def,
+ )
+
+ reindex_all_web_items()
+ define_autocomplete_dictionary()
+
+def to_search_field(field):
+ if field == "tags":
+ return TagField("tags", separator=",")
+
+ return TextField(field)
+
+def insert_item_to_index(website_item_doc):
+ # Insert item to index
+ key = get_cache_key(website_item_doc.name)
+ r = frappe.cache()
+ web_item = create_web_item_map(website_item_doc)
+
+ for k, v in web_item.items():
+ super(RedisWrapper, r).hset(make_key(key), k, v)
+
+ insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name)
+
+def insert_to_name_ac(web_name, doc_name):
+ ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=frappe.cache())
+ ac.add_suggestions(Suggestion(web_name, payload=doc_name))
+
+def create_web_item_map(website_item_doc):
+ fields_to_index = get_fields_indexed()
+
+ web_item = {}
+
+ for f in fields_to_index:
+ web_item[f] = website_item_doc.get(f) or ''
+
+ return web_item
+
+def update_index_for_item(website_item_doc):
+ # Reinsert to Cache
+ insert_item_to_index(website_item_doc)
+ define_autocomplete_dictionary()
+
+def delete_item_from_index(website_item_doc):
+ r = frappe.cache()
+ key = get_cache_key(website_item_doc.name)
+
+ try:
+ r.delete(key)
+ except:
+ return False
+
+ delete_from_ac_dict(website_item_doc)
+
+ return True
+
+def delete_from_ac_dict(website_item_doc):
+ '''Removes this items's name from autocomplete dictionary'''
+ r = frappe.cache()
+ name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=r)
+ name_ac.delete(website_item_doc.web_item_name)
+
+def define_autocomplete_dictionary():
+ """Creates an autocomplete search dictionary for `name`.
+ Also creats autocomplete dictionary for `categories` if
+ checked in E Commerce Settings"""
+
+ r = frappe.cache()
+ name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=r)
+ cat_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=r)
+
+ ac_categories = frappe.db.get_single_value(
+ 'E Commerce Settings',
+ 'show_categories_in_search_autocomplete'
+ )
+
+ # Delete both autocomplete dicts
+ try:
+ r.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE))
+ r.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE))
+ except:
+ return False
+
+ items = frappe.get_all(
+ 'Website Item',
+ fields=['web_item_name', 'item_group'],
+ filters={"published": True}
+ )
+
+ for item in items:
+ name_ac.add_suggestions(Suggestion(item.web_item_name))
+ if ac_categories and item.item_group:
+ cat_ac.add_suggestions(Suggestion(item.item_group))
+
+ return True
+
+def reindex_all_web_items():
+ items = frappe.get_all(
+ 'Website Item',
+ fields=get_fields_indexed(),
+ filters={"published": True}
+ )
+
+ r = frappe.cache()
+ for item in items:
+ web_item = create_web_item_map(item)
+ key = make_key(get_cache_key(item.name))
+
+ for k, v in web_item.items():
+ super(RedisWrapper, r).hset(key, k, v)
+
+def get_cache_key(name):
+ name = frappe.scrub(name)
+ return f"{WEBSITE_ITEM_KEY_PREFIX}{name}"
+
+def get_fields_indexed():
+ fields_to_index = frappe.db.get_single_value(
+ 'E Commerce Settings',
+ 'search_index_fields'
+ ).split(',')
+
+ mandatory_fields = ['name', 'web_item_name', 'route', 'thumbnail']
+ fields_to_index = fields_to_index + mandatory_fields
+
+ return fields_to_index
+
+# TODO: Remove later
+# # Figure out a way to run this at startup
+define_autocomplete_dictionary()
+create_website_items_index()
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 05f07f5..016f1c4 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -53,15 +53,15 @@
on_session_creation = [
"erpnext.portal.utils.create_customer_or_supplier",
- "erpnext.shopping_cart.utils.set_cart_count"
+ "erpnext.e_commerce.shopping_cart.utils.set_cart_count"
]
-on_logout = "erpnext.shopping_cart.utils.clear_cart_count"
+on_logout = "erpnext.e_commerce.shopping_cart.utils.clear_cart_count"
treeviews = ['Account', 'Cost Center', 'Warehouse', 'Item Group', 'Customer Group', 'Sales Person', 'Territory', 'Assessment Group', 'Department']
# website
-update_website_context = ["erpnext.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_context"]
-my_account_context = "erpnext.shopping_cart.utils.update_my_account_context"
+update_website_context = ["erpnext.e_commerce.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_context"]
+my_account_context = "erpnext.e_commerce.shopping_cart.utils.update_my_account_context"
webform_list_context = "erpnext.controllers.website_list_for_contact.get_webform_list_context"
calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "Course Schedule"]
@@ -77,7 +77,7 @@
'Services': 'erpnext.domains.services',
}
-website_generators = ["Item Group", "Item", "BOM", "Sales Partner",
+website_generators = ["Item Group", "Website Item", "BOM", "Sales Partner",
"Job Opening", "Student Admission"]
website_context = {
@@ -240,11 +240,11 @@
"erpnext.support.doctype.issue.issue.set_first_response_time"
]
},
- "Sales Taxes and Charges Template": {
- "on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings"
+ ("Sales Taxes and Charges Template", "Price List"): {
+ "on_update": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.validate_cart_settings"
},
"Website Settings": {
- "validate": "erpnext.portal.doctype.products_settings.products_settings.home_page_is_products"
+ "validate": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.home_page_is_products"
},
"Tax Category": {
"validate": "erpnext.regional.india.utils.validate_tax_category"
diff --git a/erpnext/modules.txt b/erpnext/modules.txt
index a9f94ce..2a0ee6b 100644
--- a/erpnext/modules.txt
+++ b/erpnext/modules.txt
@@ -9,7 +9,6 @@
Stock
Support
Utilities
-Shopping Cart
Assets
Portal
Maintenance
@@ -26,3 +25,4 @@
Loan Management
Payroll
Telephony
+E-commerce
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index f15c65e..6895932 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -303,3 +303,6 @@
erpnext.patches.v13_0.fix_additional_cost_in_mfg_stock_entry
erpnext.patches.v13_0.set_status_in_maintenance_schedule_table
erpnext.patches.v13_0.add_default_interview_notification_templates
+erpnext.patches.v13_0.create_website_items
+erpnext.patches.v13_0.populate_e_commerce_settings
+erpnext.patches.v13_0.make_homepage_products_website_items
diff --git a/erpnext/patches/v13_0/create_website_items.py b/erpnext/patches/v13_0/create_website_items.py
new file mode 100644
index 0000000..a3b0751
--- /dev/null
+++ b/erpnext/patches/v13_0/create_website_items.py
@@ -0,0 +1,45 @@
+from __future__ import unicode_literals
+import frappe
+
+from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
+
+def execute():
+ frappe.reload_doc("e_commerce", "doctype", "website_item")
+ frappe.reload_doc("stock", "doctype", "item")
+
+ web_fields_to_map = ["route", "slideshow", "website_image", "website_image_alt",
+ "website_warehouse", "web_long_description", "website_content"]
+
+ items = frappe.db.sql("""
+ Select
+ item_code, item_name, item_group, stock_uom, brand, image,
+ has_variants, variant_of, description, weightage,
+ route, slideshow, website_image_alt,
+ website_warehouse, web_long_description, website_content
+ from
+ `tabItem`
+ where
+ show_in_website = 1
+ or show_variant_in_website = 1""", as_dict=1)
+
+ for item in items:
+ if frappe.db.exists("Website Item", {"item_code": item.item_code}):
+ continue
+
+ # make website item from item (publish item)
+ website_item = make_website_item(item, save=False)
+ website_item.ranking = item.get("weightage")
+ for field in web_fields_to_map:
+ website_item.update({field: item.get(field)})
+ website_item.save()
+
+ # move Website Item Group & Website Specification table to Website Item
+ for doc in ("Website Item Group", "Item Website Specification"):
+ frappe.db.sql("""Update `tab{doctype}`
+ set
+ parenttype = 'Website Item',
+ parent = '{web_item}'
+ where
+ parenttype = 'Item'
+ and parent = '{item}'
+ """.format(doctype=doc, web_item=website_item.name, item=item.item_code))
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/make_homepage_products_website_items.py b/erpnext/patches/v13_0/make_homepage_products_website_items.py
new file mode 100644
index 0000000..8b51cad
--- /dev/null
+++ b/erpnext/patches/v13_0/make_homepage_products_website_items.py
@@ -0,0 +1,14 @@
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ homepage = frappe.get_doc("Homepage")
+
+ for row in homepage.products:
+ web_item = frappe.db.get_value("Website Item", {"item_code": row.item_code}, "name")
+ if not web_item:
+ continue
+
+ row.item_code = web_item
+
+ homepage.save()
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/populate_e_commerce_settings.py b/erpnext/patches/v13_0/populate_e_commerce_settings.py
new file mode 100644
index 0000000..19f91ef
--- /dev/null
+++ b/erpnext/patches/v13_0/populate_e_commerce_settings.py
@@ -0,0 +1,58 @@
+from __future__ import unicode_literals
+import frappe
+from frappe.utils import cint
+
+def execute():
+ frappe.reload_doc("e_commerce", "doctype", "e_commerce_settings")
+
+ products_settings_fields = [
+ "hide_variants", "home_page_is_products", "products_per_page",
+ "show_availability_status", "enable_attribute_filters", "enable_field_filters"
+ ]
+
+ shopping_cart_settings_fields = [
+ "enabled", "show_attachments", "show_price",
+ "show_stock_availability", "enable_variants", "show_contact_us_button",
+ "show_quantity_in_website", "show_apply_coupon_code_in_website",
+ "allow_items_not_in_stock", "company", "price_list", "default_customer_group",
+ "quotation_series", "enable_checkout", "payment_success_url",
+ "payment_gateway_account", "save_quotations_as_draft"
+ ]
+
+ settings = frappe.get_doc("E Commerce Settings")
+
+ def map_into_e_commerce_settings(doctype, fields):
+ data = frappe.db.sql("""
+ Select
+ field, value
+ from `tabSingles`
+ where
+ doctype='{doctype}'
+ and field in ({fields})
+ """.format(
+ doctype=doctype,
+ fields=(",").join(['%s'] * len(fields))
+ ), tuple(fields), as_dict=1)
+
+ # {'enable_attribute_filters': '1', ...}
+ mapper = {row.field: row.value for row in data}
+
+ for key, value in mapper.items():
+ value = cint(value) if (value and value.isdigit()) else value
+ settings.update({key: value})
+
+ settings.save()
+
+ # shift data to E Commerce Settings
+ map_into_e_commerce_settings("Products Settings", products_settings_fields)
+ map_into_e_commerce_settings("Shopping Cart Settings", shopping_cart_settings_fields)
+
+ # move filters and attributes tables to E Commerce Settings from Products Settings
+ for doctype in ("Website Filter Field", "Website Attribute"):
+ frappe.db.sql("""Update `tab{doctype}`
+ set
+ parenttype = 'E Commerce Settings',
+ parent = 'E Commerce Settings'
+ where
+ parent = 'Products Settings'
+ """.format(doctype=doctype))
\ No newline at end of file
diff --git a/erpnext/portal/doctype/homepage/homepage.js b/erpnext/portal/doctype/homepage/homepage.js
index c7c66e0..59f808a 100644
--- a/erpnext/portal/doctype/homepage/homepage.js
+++ b/erpnext/portal/doctype/homepage/homepage.js
@@ -3,9 +3,9 @@
frappe.ui.form.on('Homepage', {
setup: function(frm) {
- frm.fields_dict["products"].grid.get_field("item_code").get_query = function(){
+ frm.fields_dict["products"].grid.get_field("item").get_query = function() {
return {
- filters: {'show_in_website': 1}
+ filters: {'published': 1}
}
}
},
@@ -21,11 +21,10 @@
});
frappe.ui.form.on('Homepage Featured Product', {
-
- view: function(frm, cdt, cdn){
- var child= locals[cdt][cdn]
- if(child.item_code && frm.doc.products_url){
- window.location.href = frm.doc.products_url + '/' + encodeURIComponent(child.item_code);
+ view: function(frm, cdt, cdn) {
+ var child= locals[cdt][cdn];
+ if (child.item_code && child.route) {
+ window.open('/' + child.route, '_blank');
}
}
});
diff --git a/erpnext/portal/doctype/homepage/homepage.json b/erpnext/portal/doctype/homepage/homepage.json
index ad27278..73f816d 100644
--- a/erpnext/portal/doctype/homepage/homepage.json
+++ b/erpnext/portal/doctype/homepage/homepage.json
@@ -1,518 +1,143 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "",
+ "actions": [],
"beta": 1,
"creation": "2016-04-22 05:27:52.109319",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
- "editable_grid": 0,
"engine": "InnoDB",
+ "field_order": [
+ "company",
+ "hero_section_based_on",
+ "column_break_2",
+ "title",
+ "section_break_4",
+ "tag_line",
+ "description",
+ "hero_image",
+ "slideshow",
+ "hero_section",
+ "products_section",
+ "products_url",
+ "products"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "company",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Company",
- "length": 0,
- "no_copy": 0,
"options": "Company",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "hero_section_based_on",
"fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Hero Section Based On",
- "length": 0,
- "no_copy": 0,
- "options": "Default\nSlideshow\nHomepage Section",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Default\nSlideshow\nHomepage Section"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
"fieldname": "title",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Title",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Title"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
"fieldname": "section_break_4",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Hero Section",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Hero Section"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:doc.hero_section_based_on === 'Default'",
"description": "Company Tagline for website homepage",
"fieldname": "tag_line",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Tag Line",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:doc.hero_section_based_on === 'Default'",
"description": "Company Description for website homepage",
"fieldname": "description",
"fieldtype": "Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Description",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:doc.hero_section_based_on === 'Default'",
"fieldname": "hero_image",
"fieldtype": "Attach Image",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Hero Image",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Hero Image"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:doc.hero_section_based_on === 'Slideshow'",
- "description": "",
"fieldname": "slideshow",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Homepage Slideshow",
- "length": 0,
- "no_copy": 0,
- "options": "Website Slideshow",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Website Slideshow"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:doc.hero_section_based_on === 'Homepage Section'",
"fieldname": "hero_section",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Homepage Section",
- "length": 0,
- "no_copy": 0,
- "options": "Homepage Section",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Homepage Section"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
"fieldname": "products_section",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Products",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Products"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "/products",
+ "default": "/all-products",
"fieldname": "products_url",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "URL for \"All Products\"",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "URL for \"All Products\""
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"description": "Products to be shown on website homepage",
"fieldname": "products",
"fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Products",
- "length": 0,
- "no_copy": 0,
"options": "Homepage Featured Product",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "40px"
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
"issingle": 1,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-03-02 23:12:59.676202",
+ "links": [],
+ "modified": "2021-02-18 13:29:29.531639",
"modified_by": "Administrator",
"module": "Portal",
"name": "Homepage",
- "name_case": "",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
- "report": 0,
"role": "System Manager",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
},
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
- "report": 0,
"role": "Administrator",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
}
],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "company",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/portal/doctype/homepage/homepage.py b/erpnext/portal/doctype/homepage/homepage.py
index 7eeaf4b..74e0489 100644
--- a/erpnext/portal/doctype/homepage/homepage.py
+++ b/erpnext/portal/doctype/homepage/homepage.py
@@ -16,12 +16,14 @@
delete_page_cache('home')
def setup_items(self):
- for d in frappe.get_all('Item', fields=['name', 'item_name', 'description', 'image'],
- filters={'show_in_website': 1}, limit=3):
+ for d in frappe.get_all('Website Item', fields=['name', 'item_name', 'description', 'image', 'route'],
+ filters={'published': 1}, limit=3):
- doc = frappe.get_doc('Item', d.name)
+ doc = frappe.get_doc('Website Item', d.name)
if not doc.route:
# set missing route
doc.save()
self.append('products', dict(item_code=d.name,
- item_name=d.item_name, description=d.description, image=d.image))
+ item_name=d.item_name, description=d.description,
+ image=d.image, route=d.route))
+
diff --git a/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json b/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json
index 01c32ef..63789e3 100644
--- a/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json
+++ b/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json
@@ -25,10 +25,10 @@
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
- "label": "Item Code",
+ "label": "Item",
"oldfieldname": "item_code",
"oldfieldtype": "Link",
- "options": "Item",
+ "options": "Website Item",
"print_width": "150px",
"reqd": 1,
"search_index": 1,
@@ -63,7 +63,7 @@
"collapsible": 1,
"fieldname": "section_break_5",
"fieldtype": "Section Break",
- "label": "Description"
+ "label": "Details"
},
{
"fetch_from": "item_code.web_long_description",
@@ -89,12 +89,14 @@
"label": "Image"
},
{
+ "fetch_from": "item_code.thumbnail",
"fieldname": "thumbnail",
"fieldtype": "Attach Image",
"hidden": 1,
"label": "Thumbnail"
},
{
+ "fetch_from": "item_code.route",
"fieldname": "route",
"fieldtype": "Small Text",
"label": "route",
@@ -104,7 +106,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-08-25 15:27:49.573537",
+ "modified": "2021-02-18 13:05:50.669311",
"modified_by": "Administrator",
"module": "Portal",
"name": "Homepage Featured Product",
diff --git a/erpnext/portal/doctype/products_settings/products_settings.js b/erpnext/portal/doctype/products_settings/products_settings.js
deleted file mode 100644
index 2f8b037..0000000
--- a/erpnext/portal/doctype/products_settings/products_settings.js
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Products Settings', {
- refresh: function(frm) {
- frappe.model.with_doctype('Item', () => {
- const item_meta = frappe.get_meta('Item');
-
- const valid_fields = item_meta.fields.filter(
- df => ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden
- ).map(df => ({ label: df.label, value: df.fieldname }));
-
- frm.fields_dict.filter_fields.grid.update_docfield_property(
- 'fieldname', 'fieldtype', 'Select'
- );
- frm.fields_dict.filter_fields.grid.update_docfield_property(
- 'fieldname', 'options', valid_fields
- );
- });
- }
-});
diff --git a/erpnext/portal/doctype/products_settings/products_settings.json b/erpnext/portal/doctype/products_settings/products_settings.json
deleted file mode 100644
index 2cf8431..0000000
--- a/erpnext/portal/doctype/products_settings/products_settings.json
+++ /dev/null
@@ -1,389 +0,0 @@
-{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2016-04-22 09:11:55.272398",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 0,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "If checked, the Home page will be the default Item Group for the website",
- "fieldname": "home_page_is_products",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Home Page is Products",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "show_availability_status",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Show Availability Status",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_5",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Product Page",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "6",
- "fieldname": "products_per_page",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Products per Page",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "enable_field_filters",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Enable Field Filters",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "enable_field_filters",
- "fieldname": "filter_fields",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Item Fields",
- "length": 0,
- "no_copy": 0,
- "options": "Website Filter Field",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "enable_attribute_filters",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Enable Attribute Filters",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "enable_attribute_filters",
- "fieldname": "filter_attributes",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Attributes",
- "length": 0,
- "no_copy": 0,
- "options": "Website Attribute",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "hide_variants",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Hide Variants",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 1,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-03-07 19:18:31.822309",
- "modified_by": "Administrator",
- "module": "Portal",
- "name": "Products Settings",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 0,
- "role": "Website Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/portal/doctype/products_settings/products_settings.py b/erpnext/portal/doctype/products_settings/products_settings.py
deleted file mode 100644
index d4f09b9..0000000
--- a/erpnext/portal/doctype/products_settings/products_settings.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-
-import frappe
-from frappe import _
-from frappe.model.document import Document
-from frappe.utils import cint
-
-
-class ProductsSettings(Document):
- def validate(self):
- if self.home_page_is_products:
- frappe.db.set_value("Website Settings", None, "home_page", "products")
- elif frappe.db.get_single_value("Website Settings", "home_page") == 'products':
- frappe.db.set_value("Website Settings", None, "home_page", "home")
-
- self.validate_field_filters()
- self.validate_attribute_filters()
- frappe.clear_document_cache("Product Settings", "Product Settings")
-
- def validate_field_filters(self):
- if not (self.enable_field_filters and self.filter_fields): return
-
- item_meta = frappe.get_meta('Item')
- valid_fields = [df.fieldname for df in item_meta.fields if df.fieldtype in ['Link', 'Table MultiSelect']]
-
- for f in self.filter_fields:
- if f.fieldname not in valid_fields:
- frappe.throw(_('Filter Fields Row #{0}: Fieldname <b>{1}</b> must be of type "Link" or "Table MultiSelect"').format(f.idx, f.fieldname))
-
- def validate_attribute_filters(self):
- if not (self.enable_attribute_filters and self.filter_attributes): return
-
- # if attribute filters are enabled, hide_variants should be disabled
- self.hide_variants = 0
-
-
-def home_page_is_products(doc, method):
- '''Called on saving Website Settings'''
- home_page_is_products = cint(frappe.db.get_single_value('Products Settings', 'home_page_is_products'))
- if home_page_is_products:
- doc.home_page = 'products'
diff --git a/erpnext/portal/doctype/products_settings/test_products_settings.py b/erpnext/portal/doctype/products_settings/test_products_settings.py
deleted file mode 100644
index 5495cc9..0000000
--- a/erpnext/portal/doctype/products_settings/test_products_settings.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-from __future__ import unicode_literals
-
-import unittest
-
-
-class TestProductsSettings(unittest.TestCase):
- pass
diff --git a/erpnext/portal/doctype/website_attribute/website_attribute.json b/erpnext/portal/doctype/website_attribute/website_attribute.json
index 2874dc4..eed33ec 100644
--- a/erpnext/portal/doctype/website_attribute/website_attribute.json
+++ b/erpnext/portal/doctype/website_attribute/website_attribute.json
@@ -1,76 +1,32 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2019-01-01 13:04:54.479079",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2019-01-01 13:04:54.479079",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "attribute"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "attribute",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Attribute",
- "length": 0,
- "no_copy": 0,
- "options": "Item Attribute",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "attribute",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Attribute",
+ "options": "Item Attribute",
+ "reqd": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2019-01-01 13:04:59.715572",
- "modified_by": "Administrator",
- "module": "Portal",
- "name": "Website Attribute",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2021-02-18 13:18:57.810536",
+ "modified_by": "Administrator",
+ "module": "Portal",
+ "name": "Website Attribute",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/portal/product_configurator/test_product_configurator.py b/erpnext/portal/product_configurator/test_product_configurator.py
deleted file mode 100644
index 5db74f2..0000000
--- a/erpnext/portal/product_configurator/test_product_configurator.py
+++ /dev/null
@@ -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})
-
- products_settings = frappe.get_doc('Products Settings')
- products_settings.enable_field_filters = 1
- products_settings.append('filter_fields', {'fieldname': 'item_group'})
- products_settings.append('filter_fields', {'fieldname': 'stock_uom'})
- products_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()
diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py
deleted file mode 100644
index cf623c8..0000000
--- a/erpnext/portal/product_configurator/utils.py
+++ /dev/null
@@ -1,446 +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
-
-
-def get_field_filter_data():
- product_settings = get_product_settings()
- filter_fields = [row.fieldname for row in product_settings.filter_fields]
-
- meta = frappe.get_meta('Item')
- fields = [df for df in meta.fields if df.fieldname in filter_fields]
-
- filter_data = []
- for f in fields:
- doctype = f.get_link_doctype()
-
- # apply enable/disable/show_in_website filter
- meta = frappe.get_meta(doctype)
- filters = {}
- if meta.has_field('enabled'):
- filters['enabled'] = 1
- if meta.has_field('disabled'):
- filters['disabled'] = 0
- if meta.has_field('show_in_website'):
- filters['show_in_website'] = 1
-
- values = [d.name for d in frappe.get_all(doctype, filters)]
- filter_data.append([f, values])
-
- return filter_data
-
-
-def get_attribute_filter_data():
- product_settings = get_product_settings()
- attributes = [row.attribute for row in product_settings.filter_attributes]
- attribute_docs = [
- frappe.get_doc('Item Attribute', attribute) for attribute in attributes
- ]
-
- # mark attribute values as checked if they are present in the request url
- if frappe.form_dict:
- for attr in attribute_docs:
- if attr.name in frappe.form_dict:
- value = frappe.form_dict[attr.name]
- if value:
- enabled_values = value.split(',')
- else:
- enabled_values = []
-
- for v in enabled_values:
- for item_attribute_row in attr.item_attribute_values:
- if v == item_attribute_row.attribute_value:
- item_attribute_row.checked = True
-
- return attribute_docs
-
-
-def get_products_for_website(field_filters=None, attribute_filters=None, search=None):
- if attribute_filters:
- item_codes = get_item_codes_by_attributes(attribute_filters)
- items_by_attributes = get_items([['name', 'in', item_codes]])
-
- if field_filters:
- items_by_fields = get_items_by_fields(field_filters)
-
- if attribute_filters and not field_filters:
- return items_by_attributes
-
- if field_filters and not attribute_filters:
- return items_by_fields
-
- if field_filters and attribute_filters:
- items_intersection = []
- item_codes_in_attribute = [item.name for item in items_by_attributes]
-
- for item in items_by_fields:
- if item.name in item_codes_in_attribute:
- items_intersection.append(item)
-
- return items_intersection
-
- if search:
- return get_items(search=search)
-
- return get_items()
-
-
-@frappe.whitelist(allow_guest=True)
-def get_products_html_for_website(field_filters=None, attribute_filters=None):
- field_filters = frappe.parse_json(field_filters)
- attribute_filters = frappe.parse_json(attribute_filters)
- set_item_group_filters(field_filters)
-
- items = get_products_for_website(field_filters, attribute_filters)
- html = ''.join(get_html_for_items(items))
-
- if not items:
- html = frappe.render_template('erpnext/www/all-products/not_found.html', {})
-
- return html
-
-def set_item_group_filters(field_filters):
- if field_filters is not None and 'item_group' in field_filters:
- field_filters['item_group'] = [ig[0] for ig in get_child_groups(field_filters['item_group'])]
-
-
-def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
- items = []
-
- for attribute, values in attribute_filters.items():
- attribute_values = values
-
- if not isinstance(attribute_values, list):
- attribute_values = [attribute_values]
-
- if not attribute_values: continue
-
- wheres = []
- query_values = []
- for attribute_value in attribute_values:
- wheres.append('( attribute = %s and attribute_value = %s )')
- query_values += [attribute, attribute_value]
-
- attribute_query = ' or '.join(wheres)
-
- if template_item_code:
- variant_of_query = 'AND t2.variant_of = %s'
- query_values.append(template_item_code)
- else:
- variant_of_query = ''
-
- query = '''
- SELECT
- t1.parent
- FROM
- `tabItem Variant Attribute` t1
- WHERE
- 1 = 1
- AND (
- {attribute_query}
- )
- AND EXISTS (
- SELECT
- 1
- FROM
- `tabItem` t2
- WHERE
- t2.name = t1.parent
- {variant_of_query}
- )
- GROUP BY
- t1.parent
- ORDER BY
- NULL
- '''.format(attribute_query=attribute_query, variant_of_query=variant_of_query)
-
- item_codes = set([r[0] for r in frappe.db.sql(query, query_values)])
- items.append(item_codes)
-
- res = list(set.intersection(*items))
-
- return res
-
-
-@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)
-
-
-def get_items_by_fields(field_filters):
- meta = frappe.get_meta('Item')
- filters = []
- for fieldname, values in field_filters.items():
- if not values: continue
-
- _doctype = 'Item'
- _fieldname = fieldname
-
- df = meta.get_field(fieldname)
- if df.fieldtype == 'Table MultiSelect':
- child_doctype = df.options
- child_meta = frappe.get_meta(child_doctype)
- fields = child_meta.get("fields", { "fieldtype": "Link", "in_list_view": 1 })
- if fields:
- _doctype = child_doctype
- _fieldname = fields[0].fieldname
-
- if len(values) == 1:
- filters.append([_doctype, _fieldname, '=', values[0]])
- else:
- filters.append([_doctype, _fieldname, 'in', values])
-
- return get_items(filters)
-
-
-def get_items(filters=None, search=None):
- start = frappe.form_dict.get('start', 0)
- products_settings = get_product_settings()
- page_length = products_settings.products_per_page
-
- filters = filters or []
- # convert to list of filters
- if isinstance(filters, dict):
- filters = [['Item', fieldname, '=', value] for fieldname, value in filters.items()]
-
- enabled_items_filter = get_conditions({ 'disabled': 0 }, 'and')
-
- show_in_website_condition = ''
- if products_settings.hide_variants:
- show_in_website_condition = get_conditions({'show_in_website': 1 }, 'and')
- else:
- show_in_website_condition = get_conditions([
- ['show_in_website', '=', 1],
- ['show_variant_in_website', '=', 1]
- ], 'or')
-
- search_condition = ''
- if search:
- # Default fields to search from
- default_fields = {'name', 'item_name', 'description', 'item_group'}
-
- # Get meta search fields
- meta = frappe.get_meta("Item")
- meta_fields = set(meta.get_search_fields())
-
- # Join the meta fields and default fields set
- search_fields = default_fields.union(meta_fields)
- try:
- if frappe.db.count('Item', cache=True) > 50000:
- search_fields.remove('description')
- except KeyError:
- pass
-
- # Build or filters for query
- search = '%{}%'.format(search)
- or_filters = [[field, 'like', search] for field in search_fields]
-
- search_condition = get_conditions(or_filters, 'or')
-
- filter_condition = get_conditions(filters, 'and')
-
- where_conditions = ' and '.join(
- [condition for condition in [enabled_items_filter, show_in_website_condition, \
- search_condition, filter_condition] if condition]
- )
-
- left_joins = []
- for f in filters:
- if len(f) == 4 and f[0] != 'Item':
- left_joins.append(f[0])
-
- left_join = ' '.join(['LEFT JOIN `tab{0}` on (`tab{0}`.parent = `tabItem`.name)'.format(l) for l in left_joins])
-
- results = frappe.db.sql('''
- SELECT
- `tabItem`.`name`, `tabItem`.`item_name`, `tabItem`.`item_code`,
- `tabItem`.`website_image`, `tabItem`.`image`,
- `tabItem`.`web_long_description`, `tabItem`.`description`,
- `tabItem`.`route`, `tabItem`.`item_group`
- FROM
- `tabItem`
- {left_join}
- WHERE
- {where_conditions}
- GROUP BY
- `tabItem`.`name`
- ORDER BY
- `tabItem`.`weightage` DESC
- LIMIT
- {page_length}
- OFFSET
- {start}
- '''.format(
- where_conditions=where_conditions,
- start=start,
- page_length=page_length,
- left_join=left_join
- )
- , as_dict=1)
-
- for r in results:
- r.description = r.web_long_description or r.description
- r.image = r.website_image or r.image
- product_info = get_product_info_for_website(r.item_code, skip_quotation_creation=True).get('product_info')
- if product_info:
- r.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None
-
- return results
-
-
-def get_conditions(filter_list, and_or='and'):
- from frappe.model.db_query import DatabaseQuery
-
- if not filter_list:
- return ''
-
- conditions = []
- DatabaseQuery('Item').build_filter_conditions(filter_list, conditions, ignore_permissions=True)
- join_by = ' {0} '.format(and_or)
-
- return '(' + join_by.join(conditions) + ')'
-
-# 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
-
-def get_html_for_items(items):
- html = []
- for item in items:
- html.append(frappe.render_template('erpnext/www/all-products/item_row.html', {
- 'item': item
- }))
- return html
-
-def get_product_settings():
- doc = frappe.get_cached_doc('Products Settings')
- doc.products_per_page = doc.products_per_page or 20
- return doc
diff --git a/erpnext/portal/utils.py b/erpnext/portal/utils.py
index bae8f35..a87471f 100644
--- a/erpnext/portal/utils.py
+++ b/erpnext/portal/utils.py
@@ -1,10 +1,8 @@
-from __future__ import unicode_literals
-
import frappe
from frappe.utils.nestedset import get_root_of
-from erpnext.shopping_cart.cart import get_debtors_account
-from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
+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,
)
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index 6b70dab..a891121 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -11,7 +11,8 @@
],
"js/erpnext-web.min.js": [
"public/js/website_utils.js",
- "public/js/shopping_cart.js"
+ "public/js/shopping_cart.js",
+ "public/js/wishlist.js"
],
"css/erpnext-web.css": [
"public/scss/website.scss",
diff --git a/erpnext/public/js/conf.js b/erpnext/public/js/conf.js
index eb709e5..a0f56a2 100644
--- a/erpnext/public/js/conf.js
+++ b/erpnext/public/js/conf.js
@@ -21,6 +21,6 @@
'Geo': 'Settings',
'Portal': 'Website',
'Utilities': 'Settings',
- 'Shopping Cart': 'Website',
+ 'E-commerce': 'Website',
'Contacts': 'CRM'
});
diff --git a/erpnext/public/js/shopping_cart.js b/erpnext/public/js/shopping_cart.js
index 6a923ae..b57862b 100644
--- a/erpnext/public/js/shopping_cart.js
+++ b/erpnext/public/js/shopping_cart.js
@@ -63,7 +63,7 @@
$(".shopping-cart").on('shown.bs.dropdown', function() {
if (!$('.shopping-cart-menu .cart-container').length) {
return frappe.call({
- method: 'erpnext.shopping_cart.cart.get_shopping_cart_menu',
+ method: 'erpnext.e_commerce.shopping_cart.cart.get_shopping_cart_menu',
callback: function(r) {
if (r.message) {
$('.shopping-cart-menu').html(r.message);
@@ -83,7 +83,7 @@
} else {
return frappe.call({
type: "POST",
- method: "erpnext.shopping_cart.cart.update_cart",
+ method: "erpnext.e_commerce.shopping_cart.cart.update_cart",
args: {
item_code: opts.item_code,
qty: opts.qty,
@@ -93,9 +93,6 @@
btn: opts.btn,
callback: function(r) {
shopping_cart.set_cart_count();
- if (r.message.shopping_cart_menu) {
- $('.shopping-cart-menu').html(r.message.shopping_cart_menu);
- }
if(opts.callback)
opts.callback(r);
}
@@ -129,6 +126,10 @@
if(cart_count) {
$badge.html(cart_count);
+ $cart.addClass('cart-animate');
+ setTimeout(() => {
+ $cart.removeClass('cart-animate');
+ }, 500);
} else {
$badge.remove();
}
@@ -180,10 +181,45 @@
show_cart_navbar: function () {
frappe.call({
- method: "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.is_cart_enabled",
+ method: "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.is_cart_enabled",
callback: function(r) {
$(".shopping-cart").toggleClass('hidden', r.message ? false : true);
}
});
+ },
+
+ animate_add_to_cart(button) {
+ // Create 'added to cart' animation
+ let btn_id = "#" + button[0].id;
+ this.toggle_button_class(button, 'not-added', 'added-to-cart');
+ $(btn_id).text('Added to Cart');
+
+ // undo
+ setTimeout(() => {
+ this.toggle_button_class(button, 'added-to-cart', 'not-added');
+ $(btn_id).text('Add to Cart');
+ }, 2000);
+ },
+
+ toggle_button_class(button, remove, add) {
+ button.removeClass(remove);
+ button.addClass(add);
+ },
+
+ bind_add_to_cart_action() {
+ $('.page_content').on('click', '.btn-add-to-cart-list', (e) => {
+ const $btn = $(e.currentTarget);
+ $btn.prop('disabled', true);
+
+ this.animate_add_to_cart($btn);
+
+ const item_code = $btn.data('item-code');
+ erpnext.shopping_cart.update_cart({
+ item_code,
+ qty: 1
+ });
+
+ });
}
+
});
diff --git a/erpnext/public/js/wishlist.js b/erpnext/public/js/wishlist.js
new file mode 100644
index 0000000..6bcb6b1
--- /dev/null
+++ b/erpnext/public/js/wishlist.js
@@ -0,0 +1,162 @@
+frappe.provide("erpnext.wishlist");
+var wishlist = erpnext.wishlist;
+
+frappe.provide("erpnext.shopping_cart");
+var shopping_cart = erpnext.shopping_cart;
+
+$.extend(wishlist, {
+ set_wishlist_count: function() {
+ // set badge count for wishlist icon
+ var wish_count = frappe.get_cookie("wish_count");
+ if (frappe.session.user==="Guest") {
+ wish_count = 0;
+ }
+
+ if (wish_count) {
+ $(".wishlist").toggleClass('hidden', false);
+ }
+
+ var $wishlist = $('.wishlist-icon');
+ var $badge = $wishlist.find("#wish-count");
+
+ if (parseInt(wish_count) === 0 || wish_count === undefined) {
+ $wishlist.css("display", "none");
+ } else {
+ $wishlist.css("display", "inline");
+ }
+ if (wish_count) {
+ $badge.html(wish_count);
+ $wishlist.addClass('cart-animate');
+ setTimeout(() => {
+ $wishlist.removeClass('cart-animate');
+ }, 500);
+ } else {
+ $badge.remove();
+ }
+ },
+
+ bind_move_to_cart_action: function() {
+ // move item to cart from wishlist
+ $('.page_content').on("click", ".btn-add-to-cart", (e) => {
+ const $move_to_cart_btn = $(e.currentTarget);
+ let item_code = $move_to_cart_btn.data("item-code");
+
+ shopping_cart.shopping_cart_update({
+ item_code,
+ qty: 1,
+ cart_dropdown: true
+ });
+
+ let success_action = function() {
+ const $card_wrapper = $move_to_cart_btn.closest(".wishlist-card");
+ $card_wrapper.addClass("wish-removed");
+ };
+ let args = { item_code: item_code };
+ this.add_remove_from_wishlist("remove", args, success_action, null, true);
+ });
+ },
+
+ bind_remove_action: function() {
+ // remove item from wishlist
+ $('.page_content').on("click", ".remove-wish", (e) => {
+ const $remove_wish_btn = $(e.currentTarget);
+ let item_code = $remove_wish_btn.data("item-code");
+
+ let success_action = function() {
+ const $card_wrapper = $remove_wish_btn.closest(".wishlist-card");
+ $card_wrapper.addClass("wish-removed");
+ };
+ let args = { item_code: item_code };
+ this.add_remove_from_wishlist("remove", args, success_action);
+ });
+ },
+
+ bind_wishlist_action() {
+ // 'wish'('like') or 'unwish' item in product listing
+ $('.page_content').on('click', '.like-action', (e) => {
+ const $btn = $(e.currentTarget);
+ const $wish_icon = $btn.find('.wish-icon');
+ let me = this;
+
+ let success_action = function() {
+ erpnext.wishlist.set_wishlist_count();
+ };
+
+ if ($wish_icon.hasClass('wished')) {
+ // un-wish item
+ $btn.removeClass("like-animate");
+ this.toggle_button_class($wish_icon, 'wished', 'not-wished');
+
+ let args = { item_code: $btn.data('item-code') };
+ let failure_action = function() {
+ me.toggle_button_class($wish_icon, 'not-wished', 'wished');
+ };
+ this.add_remove_from_wishlist("remove", args, success_action, failure_action);
+ } else {
+ // wish item
+ $btn.addClass("like-animate");
+ this.toggle_button_class($wish_icon, 'not-wished', 'wished');
+
+ let args = {
+ item_code: $btn.data('item-code'),
+ price: $btn.data('price'),
+ formatted_price: $btn.data('formatted-price')
+ };
+ let failure_action = function() {
+ me.toggle_button_class($wish_icon, 'wished', 'not-wished');
+ };
+ this.add_remove_from_wishlist("add", args, success_action, failure_action);
+ }
+ });
+ },
+
+ toggle_button_class(button, remove, add) {
+ button.removeClass(remove);
+ button.addClass(add);
+ },
+
+ add_remove_from_wishlist(action, args, success_action, failure_action, async=false) {
+ /* AJAX call to add or remove Item from Wishlist
+ action: "add" or "remove"
+ args: args for method (item_code, price, formatted_price),
+ success_action: method to execute on successs,
+ failure_action: method to execute on failure,
+ async: make call asynchronously (true/false). */
+ let method = "erpnext.e_commerce.doctype.wishlist.wishlist.add_to_wishlist";
+ if (action === "remove") {
+ method = "erpnext.e_commerce.doctype.wishlist.wishlist.remove_from_wishlist";
+ }
+
+ frappe.call({
+ async: async,
+ type: "POST",
+ method: method,
+ args: args,
+ callback: function (r) {
+ if (r.exc) {
+ if (failure_action && (typeof failure_action === 'function')) {
+ failure_action();
+ }
+ frappe.msgprint({
+ message: __("Sorry, something went wrong. Please refresh."),
+ indicator: "red", title: __("Note")
+ });
+ } else if (success_action && (typeof success_action === 'function')) {
+ success_action();
+ }
+ }
+ });
+ }
+
+});
+
+frappe.ready(function() {
+ if (window.location.pathname !== "/wishlist") {
+ $(".wishlist").toggleClass('hidden', true);
+ wishlist.set_wishlist_count();
+ } else {
+ wishlist.bind_move_to_cart_action();
+ wishlist.bind_remove_action();
+ }
+
+});
\ No newline at end of file
diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
index fef1e76..04bf983 100644
--- a/erpnext/public/scss/shopping_cart.scss
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -4,13 +4,8 @@
background: var(--gray-50);
}
-
.item-breadcrumbs {
.breadcrumb-container {
- ol.breadcrumb {
- background-color: var(--gray-50) !important;
- }
-
a {
color: var(--gray-900);
}
@@ -73,7 +68,7 @@
.item-card-group-section {
.card {
- height: 360px;
+ height: 400px;
align-items: center;
justify-content: center;
@@ -140,6 +135,12 @@
.item-card {
padding: var(--padding-sm);
+ min-width: 300px;
+ }
+
+ .wishlist-card {
+ padding: var(--padding-sm);
+ min-width: 260px;
}
}
@@ -189,14 +190,40 @@
min-height: 70vh;
.product-details {
- max-width: 40%;
- margin-left: -30px;
+ max-width: 50%;
.btn-add-to-cart {
font-size: var(--text-base);
}
}
+ .expand {
+ max-width: 100% !important; // expand in absence of slideshow
+ }
+
+ @media (max-width: 789px) {
+ .product-details {
+ max-width: 90% !important;
+
+ .btn-add-to-cart {
+ font-size: var(--text-base);
+ }
+ }
+ }
+
+ .btn-add-to-wishlist {
+ svg use {
+ stroke: #F47A7A;
+ }
+ }
+
+ .btn-view-in-wishlist {
+ svg use {
+ fill: #F47A7A;
+ stroke: none;
+ }
+ }
+
.product-title {
font-size: 24px;
font-weight: 600;
@@ -323,20 +350,71 @@
}
}
-.cart-icon {
- .cart-badge {
- position: relative;
- top: -10px;
- left: -12px;
- background: var(--red-600);
- width: 16px;
- align-items: center;
- height: 16px;
- font-size: 10px;
- border-radius: 50%;
+.sub-category-container {
+ padding-bottom: 1rem;
+ margin-bottom: 1.25rem;
+ border-bottom: 1px solid var(--table-border-color);
+
+ .heading {
+ color: var(--gray-500);
}
}
+.scroll-categories {
+ white-space: nowrap;
+ overflow-x: auto;
+
+ .category-pill {
+ margin: 0px 4px;
+ display: inline-block;
+ padding: 6px 12px;
+ background-color: #ecf5fe;
+ width: fit-content;
+ font-size: 14px;
+ border-radius: 18px;
+ color: var(--blue-500);
+ }
+}
+
+
+.shopping-badge {
+ position: relative;
+ top: -10px;
+ left: -12px;
+ background: var(--red-600);
+ width: 16px;
+ align-items: center;
+ height: 16px;
+ font-size: 10px;
+ border-radius: 50%;
+}
+
+
+.cart-animate {
+ animation: wiggle 0.5s linear;
+}
+@keyframes wiggle {
+ 8%,
+ 41% {
+ transform: translateX(-10px);
+ }
+ 25%,
+ 58% {
+ transform: translate(10px);
+ }
+ 75% {
+ transform: translate(-5px);
+ }
+ 92% {
+ transform: translate(5px);
+ }
+ 0%,
+ 100% {
+ transform: translate(0);
+ }
+}
+
+
#page-cart {
.shopping-cart-header {
@@ -493,3 +571,211 @@
border: 1px solid var(--dark-border-color);
}
}
+
+.card-indicator {
+ margin-left: 6px;
+}
+
+.like-action {
+ text-align: center;
+ margin-top: -2px;
+ margin-left: 12px;
+}
+
+.like-animate {
+ animation: expand cubic-bezier(0.04, 0.4, 0.5, 0.95) 1.6s forwards 1;
+}
+
+@keyframes expand {
+ 30% {
+ transform: scale(1.6);
+ }
+ 50% {
+ transform: scale(0.8);
+ }
+ 70% {
+ transform: scale(1.3);
+ }
+ 100% {
+ transform: scale(1);
+ }
+ }
+
+@keyframes heart { 0%, 17.5% { font-size: 0; } }
+
+.not-wished {
+ cursor: pointer;
+ stroke: #F47A7A !important;
+
+ &:hover {
+ fill: #F47A7A;
+ }
+}
+
+.wished {
+ stroke: none;
+ fill: #F47A7A !important;
+}
+
+.list-row-checkbox {
+ &:before {
+ display: none;
+ }
+
+ &:checked:before {
+ display: block;
+ z-index: 1;
+ }
+}
+
+.btn-explore-variants {
+ box-shadow: none;
+ margin: var(--margin-sm) 0;
+ max-height: 50px; // to avoid resizing on window resize
+ flex: none;
+ transition: 0.3s ease;
+ color: var(--orange-500);
+ background-color: white;
+ border: 1px solid var(--orange-500);
+
+ &:hover {
+ color: white;
+ background-color: var(--orange-500);
+ }
+}
+
+.btn-add-to-cart-list{
+ box-shadow: none;
+ margin: var(--margin-sm) 0;
+ max-height: 50px; // to avoid resizing on window resize
+ flex: none;
+ transition: 0.3s ease;
+}
+
+.not-added {
+ color: var(--blue-500);
+ background-color: white;
+ border: 1px solid var(--blue-500);
+
+ &:hover {
+ background-color: var(--blue-500);
+ color: white;
+ }
+}
+
+.added-to-cart {
+ background-color: var(--dark-green-400);
+ color: white;
+ border: 2px solid var(--green-300);
+
+ &:hover {
+ color: white;
+ }
+}
+
+.wishlist-cart-not-added {
+ color: var(--blue-500);
+ background-color: white;
+ border: 1px solid var(--blue-500);
+ --icon-stroke: var(--blue-500);
+
+ &:hover {
+ background-color: var(--blue-500);
+ color: white;
+ --icon-stroke: white;
+ }
+}
+
+.remove-wish {
+ background-color: var(--gray-200);
+ position: absolute;
+ top:10px;
+ right: 20px;
+ border-radius: 50%;
+ border: 1px solid var(--gray-100);
+ width: 25px;
+ height: 25px;
+}
+
+.wish-removed {
+ display: none;
+}
+
+.item-website-specification {
+ font-size: .875rem;
+}
+
+.ratings-reviews-section {
+ border-top: 1px solid #E2E6E9;
+}
+
+.reviews-header {
+ font-size: 20px;
+ font-weight: 600;
+ color: var(--gray-800);
+}
+
+.rating-summary-title {
+ margin-top: 0.15rem;
+ font-size: 18px;
+}
+
+.user-review-title {
+ margin-top: 0.15rem;
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.rating {
+ --star-fill: var(--gray-300);
+ .star-hover {
+ --star-fill: var(--yellow-100);
+ }
+ .star-click {
+ --star-fill: var(--yellow-300);
+ }
+}
+
+.review {
+ max-width: 80%;
+ line-height: 1.6;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid #E2E6E9;
+}
+
+.review-signature {
+ display: flex;
+ font-size: 14px;
+ color: var(--gray-500);
+ font-weight: 400;
+
+ .reviewer {
+ padding-right: 8px;
+ margin-right: 8px;
+ border-right: 1px solid var(--gray-400);
+ }
+}
+
+.rating-progress-bar-section {
+ padding-bottom: 2rem;
+ border-bottom: 1px solid #E2E6E9;
+ margin-right: -10px;
+
+ .rating-bar-title {
+ margin-left: -15px;
+ }
+
+ .rating-progress-bar {
+ margin-bottom: 4px;
+ height: 7px;
+ margin-top: 6px;
+ }
+}
+
+.offer-container {
+ border: 1px solid var(--gray-300);
+ border-style: dashed;
+ border-radius: 4px;
+ padding: 6px;
+ font-size: 14px;
+}
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 7adf2cd..3f60801c 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -294,30 +294,6 @@
.format(frappe.bold(self.customer_name))
)
- def create_onboarding_docs(self, args):
- defaults = frappe.defaults.get_defaults()
- company = defaults.get('company') or \
- frappe.db.get_single_value('Global Defaults', 'default_company')
-
- for i in range(1, args.get('max_count')):
- customer = args.get('customer_name_' + str(i))
- if customer:
- try:
- doc = frappe.get_doc({
- 'doctype': self.doctype,
- 'customer_name': customer,
- 'customer_type': 'Company',
- 'customer_group': _('Commercial'),
- 'territory': defaults.get('country'),
- 'company': company
- }).insert()
-
- if args.get('customer_email_' + str(i)):
- create_contact(customer, self.doctype,
- doc.name, args.get("customer_email_" + str(i)))
- except frappe.NameError:
- pass
-
def create_contact(contact, party_type, party, email):
"""Create contact based on given contact name"""
contact = contact.split(' ')
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index 99c43bf..e9644cc 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -274,7 +274,7 @@
customer = frappe.get_doc(customer_doclist)
customer.flags.ignore_permissions = ignore_permissions
if quotation.get("party_name") == "Shopping Cart":
- customer.customer_group = frappe.db.get_value("Shopping Cart Settings", None,
+ customer.customer_group = frappe.db.get_value("E Commerce Settings", None,
"default_customer_group")
try:
diff --git a/erpnext/selling/onboarding_slide/add_a_few_customers/add_a_few_customers.json b/erpnext/selling/onboarding_slide/add_a_few_customers/add_a_few_customers.json
deleted file mode 100644
index 92d00bc..0000000
--- a/erpnext/selling/onboarding_slide/add_a_few_customers/add_a_few_customers.json
+++ /dev/null
@@ -1,49 +0,0 @@
-{
- "add_more_button": 1,
- "app": "ERPNext",
- "creation": "2019-11-15 14:44:10.065014",
- "docstatus": 0,
- "doctype": "Onboarding Slide",
- "domains": [],
- "help_links": [
- {
- "label": "Learn More",
- "video_id": "zsrrVDk6VBs"
- }
- ],
- "idx": 0,
- "image_src": "",
- "is_completed": 0,
- "max_count": 3,
- "modified": "2019-12-09 17:54:01.686006",
- "modified_by": "Administrator",
- "name": "Add A Few Customers",
- "owner": "Administrator",
- "ref_doctype": "Customer",
- "slide_desc": "",
- "slide_fields": [
- {
- "align": "",
- "fieldname": "customer_name",
- "fieldtype": "Data",
- "label": "Customer Name",
- "placeholder": "",
- "reqd": 1
- },
- {
- "align": "",
- "fieldtype": "Column Break",
- "reqd": 0
- },
- {
- "align": "",
- "fieldname": "customer_email",
- "fieldtype": "Data",
- "label": "Email ID",
- "reqd": 1
- }
- ],
- "slide_order": 40,
- "slide_title": "Add A Few Customers",
- "slide_type": "Create"
-}
\ No newline at end of file
diff --git a/erpnext/setup/doctype/brand/brand.json b/erpnext/setup/doctype/brand/brand.json
index a8f0674..45b4db8 100644
--- a/erpnext/setup/doctype/brand/brand.json
+++ b/erpnext/setup/doctype/brand/brand.json
@@ -1,270 +1,111 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "field:brand",
- "beta": 0,
- "creation": "2013-02-22 01:27:54",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 0,
+ "actions": [],
+ "allow_import": 1,
+ "allow_rename": 1,
+ "autoname": "field:brand",
+ "creation": "2013-02-22 01:27:54",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "brand",
+ "image",
+ "description",
+ "defaults",
+ "brand_defaults"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 1,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "brand",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Brand Name",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "brand",
- "oldfieldtype": "Data",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
+ "allow_in_quick_entry": 1,
+ "fieldname": "brand",
+ "fieldtype": "Data",
+ "label": "Brand Name",
+ "oldfieldname": "brand",
+ "oldfieldtype": "Data",
+ "reqd": 1,
"unique": 1
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "description",
- "fieldtype": "Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Description",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "description",
- "oldfieldtype": "Text",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "in_list_view": 1,
+ "label": "Description",
+ "oldfieldname": "description",
+ "oldfieldtype": "Text",
"width": "300px"
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "defaults",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Defaults",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "defaults",
+ "fieldtype": "Section Break",
+ "label": "Defaults"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "brand_defaults",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Brand Defaults",
- "length": 0,
- "no_copy": 0,
- "options": "Item Default",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "brand_defaults",
+ "fieldtype": "Table",
+ "label": "Brand Defaults",
+ "options": "Item Default"
+ },
+ {
+ "fieldname": "image",
+ "fieldtype": "Attach Image",
+ "hidden": 1,
+ "label": "Image"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-certificate",
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-10-23 23:18:06.067612",
- "modified_by": "Administrator",
- "module": "Setup",
- "name": "Brand",
- "owner": "Administrator",
+ ],
+ "icon": "fa fa-certificate",
+ "idx": 1,
+ "image_field": "image",
+ "links": [],
+ "modified": "2021-03-01 15:57:30.005783",
+ "modified_by": "Administrator",
+ "module": "Setup",
+ "name": "Brand",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 1,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Item Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "import": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Item Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Stock User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
- },
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock User"
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Sales User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
- },
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales User"
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Purchase User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
- },
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Purchase User"
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User"
}
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 1,
- "sort_order": "ASC",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
-}
+ ],
+ "quick_entry": 1,
+ "show_name_in_global_search": 1,
+ "sort_field": "modified",
+ "sort_order": "ASC"
+}
\ No newline at end of file
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index ab50a58..9ff9260 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -1,8 +1,6 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
-
import copy
import frappe
@@ -13,9 +11,8 @@
from frappe.website.website_generator import WebsiteGenerator
from six.moves.urllib.parse import quote
-from erpnext.shopping_cart.filters import ProductFiltersBuilder
-from erpnext.shopping_cart.product_info import set_product_info_for_website
-from erpnext.shopping_cart.product_query import ProductQuery
+from erpnext.e_commerce.filters import ProductFiltersBuilder
+from erpnext.e_commerce.product_query import ProductQuery
from erpnext.utilities.product import get_qty_in_stock
@@ -74,7 +71,8 @@
def get_context(self, context):
context.show_search=True
- context.page_length = cint(frappe.db.get_single_value('Products Settings', 'products_per_page')) or 6
+ context.page_length = cint(frappe.db.get_single_value('E Commerce Settings', 'products_per_page')) or 6
+ context.e_commerce_settings = frappe.get_cached_doc('E Commerce Settings', 'E Commerce Settings')
context.search_link = '/product_search'
if frappe.form_dict:
@@ -91,22 +89,25 @@
if not field_filters:
field_filters = {}
- # Ensure the query remains within current item group & sub group
- field_filters['item_group'] = [ig[0] for ig in get_child_groups(self.name)]
+ # Ensure the query remains within current item group
+ field_filters['item_group'] = self.name
engine = ProductQuery()
- context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name)
+ context.items, discounts = engine.query(attribute_filters, field_filters, search, start)
filter_engine = ProductFiltersBuilder(self.name)
context.field_filters = filter_engine.get_field_filters()
context.attribute_filters = filter_engine.get_attribute_filters()
+ if discounts:
+ context.discount_filters = filter_engine.get_discount_filters(discounts)
context.update({
"parents": get_parent_item_groups(self.parent_item_group),
"title": self.name
})
+ context.sub_categories = get_child_groups(self.name)
if self.slideshow:
values = {
'show_indicators': 1,
@@ -120,14 +121,13 @@
values[f"slide_{index + 1}_image"] = slide.image
values[f"slide_{index + 1}_title"] = slide.heading
values[f"slide_{index + 1}_subtitle"] = slide.description
- values[f"slide_{index + 1}_theme"] = slide.theme or "Light"
- values[f"slide_{index + 1}_content_align"] = slide.content_align or "Centre"
- values[f"slide_{index + 1}_primary_action_label"] = slide.label
+ values[f"slide_{index + 1}_theme"] = slide.get("theme") or "Light"
+ values[f"slide_{index + 1}_content_align"] = slide.get("content_align") or "Centre"
values[f"slide_{index + 1}_primary_action"] = slide.url
context.slideshow = values
- context.breadcrumbs = 0
+ context.no_breadcrumbs = False
context.title = self.website_title or self.name
return context
@@ -139,90 +139,12 @@
from erpnext.stock.doctype.item.item import validate_item_default_company_links
validate_item_default_company_links(self.item_group_defaults)
-@frappe.whitelist(allow_guest=True)
-def get_product_list_for_group(product_group=None, start=0, limit=10, search=None):
- if product_group:
- item_group = frappe.get_cached_doc('Item Group', product_group)
- if item_group.is_group:
- # return child item groups if the type is of "Is Group"
- return get_child_groups_for_list_in_html(item_group, start, limit, search)
-
- child_groups = ", ".join(frappe.db.escape(i[0]) for i in get_child_groups(product_group))
-
- # base query
- query = """select I.name, I.item_name, I.item_code, I.route, I.image, I.website_image, I.thumbnail, I.item_group,
- I.description, I.web_long_description as website_description, I.is_stock_item,
- case when (S.actual_qty - S.reserved_qty) > 0 then 1 else 0 end as in_stock, I.website_warehouse,
- I.has_batch_no
- from `tabItem` I
- left join tabBin S on I.item_code = S.item_code and I.website_warehouse = S.warehouse
- where I.show_in_website = 1
- and I.disabled = 0
- and (I.end_of_life is null or I.end_of_life='0000-00-00' or I.end_of_life > %(today)s)
- and (I.variant_of = '' or I.variant_of is null)
- and (I.item_group in ({child_groups})
- or I.name in (select parent from `tabWebsite Item Group` where item_group in ({child_groups})))
- """.format(child_groups=child_groups)
- # search term condition
- if search:
- query += """ and (I.web_long_description like %(search)s
- or I.item_name like %(search)s
- or I.name like %(search)s)"""
- search = "%" + cstr(search) + "%"
-
- query += """order by I.weightage desc, in_stock desc, I.modified desc limit %s, %s""" % (cint(start), cint(limit))
-
- data = frappe.db.sql(query, {"product_group": product_group,"search": search, "today": nowdate()}, as_dict=1)
- data = adjust_qty_for_expired_items(data)
-
- if cint(frappe.db.get_single_value("Shopping Cart Settings", "enabled")):
- for item in data:
- set_product_info_for_website(item)
-
- return data
-
-def get_child_groups_for_list_in_html(item_group, start, limit, search):
- search_filters = None
- if search_filters:
- search_filters = [
- dict(name = ('like', '%{}%'.format(search))),
- dict(description = ('like', '%{}%'.format(search)))
- ]
- data = frappe.db.get_all('Item Group',
- fields = ['name', 'route', 'description', 'image'],
- filters = dict(
- show_in_website = 1,
- parent_item_group = item_group.name,
- lft = ('>', item_group.lft),
- rgt = ('<', item_group.rgt),
- ),
- or_filters = search_filters,
- order_by = 'weightage desc, name asc',
- start = start,
- limit = limit
- )
-
- return data
-
-def adjust_qty_for_expired_items(data):
- adjusted_data = []
-
- for item in data:
- if item.get('has_batch_no') and item.get('website_warehouse'):
- stock_qty_dict = get_qty_in_stock(
- item.get('name'), 'website_warehouse', item.get('website_warehouse'))
- qty = stock_qty_dict.stock_qty[0][0] if stock_qty_dict.stock_qty else 0
- item['in_stock'] = 1 if qty else 0
- adjusted_data.append(item)
-
- return adjusted_data
-
-
def get_child_groups(item_group_name):
+ """Returns child item groups *excluding* passed group."""
item_group = frappe.get_doc("Item Group", item_group_name)
- return frappe.db.sql("""select name
- from `tabItem Group` where lft>=%(lft)s and rgt<=%(rgt)s
- and show_in_website = 1""", {"lft": item_group.lft, "rgt": item_group.rgt})
+ return frappe.db.sql("""select name, route
+ from `tabItem Group` where lft>%(lft)s and rgt<%(rgt)s
+ and show_in_website = 1""", {"lft": item_group.lft, "rgt": item_group.rgt}, as_dict=1)
def get_child_item_groups(item_group_name):
item_group = frappe.get_cached_value("Item Group",
@@ -239,31 +161,33 @@
if (context.get("website_image") or "").startswith("files/"):
context["website_image"] = "/" + quote(context["website_image"])
- context["show_availability_status"] = cint(frappe.db.get_single_value('Products Settings',
+ context["show_availability_status"] = cint(frappe.db.get_single_value('E Commerce Settings',
'show_availability_status'))
products_template = 'templates/includes/products_as_list.html'
return frappe.get_template(products_template).render(context)
-def get_group_item_count(item_group):
- child_groups = ", ".join('"' + i[0] + '"' for i in get_child_groups(item_group))
- return frappe.db.sql("""select count(*) from `tabItem`
- where docstatus = 0 and show_in_website = 1
- and (item_group in (%s)
- or name in (select parent from `tabWebsite Item Group`
- where item_group in (%s))) """ % (child_groups, child_groups))[0][0]
+def get_parent_item_groups(item_group_name, from_item=False):
+ base_nav_page = {"name": frappe._("Shop by Category"), "route":"/shop-by-category"}
-def get_parent_item_groups(item_group_name):
+ if from_item and frappe.request.environ.get("HTTP_REFERER"):
+ # base page after 'Home' will vary on Item page
+ last_page = frappe.request.environ["HTTP_REFERER"].split('/')[-1]
+ if last_page and last_page in ("shop-by-category", "all-products"):
+ base_nav_page_title = " ".join(last_page.split("-")).title()
+ base_nav_page = {"name": frappe._(base_nav_page_title), "route":"/"+last_page}
+
base_parents = [
{"name": frappe._("Home"), "route":"/"},
- {"name": frappe._("All Products"), "route":"/all-products"},
+ base_nav_page,
]
+
if not item_group_name:
return base_parents
- item_group = frappe.get_doc("Item Group", item_group_name)
+ item_group = frappe.db.get_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1)
parent_groups = frappe.db.sql("""select name, route from `tabItem Group`
where lft <= %s and rgt >= %s
and show_in_website=1
diff --git "a/erpnext/setup/onboarding_slide/welcome_back_to_erpnext\041/welcome_back_to_erpnext\041.json" "b/erpnext/setup/onboarding_slide/welcome_back_to_erpnext\041/welcome_back_to_erpnext\041.json"
deleted file mode 100644
index f00dc94..0000000
--- "a/erpnext/setup/onboarding_slide/welcome_back_to_erpnext\041/welcome_back_to_erpnext\041.json"
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "add_more_button": 0,
- "app": "ERPNext",
- "creation": "2019-12-04 19:21:39.995776",
- "docstatus": 0,
- "doctype": "Onboarding Slide",
- "domains": [],
- "help_links": [],
- "idx": 0,
- "image_src": "",
- "is_completed": 0,
- "max_count": 3,
- "modified": "2019-12-09 17:53:53.849953",
- "modified_by": "Administrator",
- "name": "Welcome back to ERPNext!",
- "owner": "Administrator",
- "slide_desc": "<p>Let's continue where you left from!</p>",
- "slide_fields": [],
- "slide_module": "Setup",
- "slide_order": 0,
- "slide_title": "Welcome back to ERPNext!",
- "slide_type": "Continue"
-}
\ No newline at end of file
diff --git "a/erpnext/setup/onboarding_slide/welcome_to_erpnext\041/welcome_to_erpnext\041.json" "b/erpnext/setup/onboarding_slide/welcome_to_erpnext\041/welcome_to_erpnext\041.json"
deleted file mode 100644
index 37eb67b..0000000
--- "a/erpnext/setup/onboarding_slide/welcome_to_erpnext\041/welcome_to_erpnext\041.json"
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "add_more_button": 0,
- "app": "ERPNext",
- "creation": "2019-11-26 17:01:26.671859",
- "docstatus": 0,
- "doctype": "Onboarding Slide",
- "domains": [],
- "help_links": [],
- "idx": 0,
- "image_src": "",
- "is_completed": 0,
- "max_count": 0,
- "modified": "2019-12-22 21:26:28.414597",
- "modified_by": "Administrator",
- "name": "Welcome to ERPNext!",
- "owner": "Administrator",
- "slide_desc": "<div class=\"text center\">Setting up an ERP can be overwhelming. But don't worry, we have got your back! This wizard will help you onboard to ERPNext in a short time!</div>",
- "slide_fields": [],
- "slide_module": "Setup",
- "slide_order": 1,
- "slide_title": "Welcome to ERPNext!",
- "slide_type": "Information"
-}
\ No newline at end of file
diff --git a/erpnext/setup/setup_wizard/operations/company_setup.py b/erpnext/setup/setup_wizard/operations/company_setup.py
index bea3906..be94994 100644
--- a/erpnext/setup/setup_wizard/operations/company_setup.py
+++ b/erpnext/setup/setup_wizard/operations/company_setup.py
@@ -33,7 +33,7 @@
def enable_shopping_cart(args):
# Needs price_lists
frappe.get_doc({
- "doctype": "Shopping Cart Settings",
+ "doctype": "E Commerce Settings",
"enabled": 1,
'company': args.get('company_name') ,
'price_list': frappe.db.get_value("Price List", {"selling": 1}),
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index c473395..fbfcb10 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -534,7 +534,7 @@
pass
def update_shopping_cart_settings(args):
- shopping_cart = frappe.get_doc("Shopping Cart Settings")
+ shopping_cart = frappe.get_doc("E Commerce Settings")
shopping_cart.update({
"enabled": 1,
'company': args.company_name,
diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/__init__.py b/erpnext/shopping_cart/doctype/shopping_cart_settings/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/shopping_cart/doctype/shopping_cart_settings/__init__.py
+++ /dev/null
diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json
deleted file mode 100644
index 7a4bb20..0000000
--- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json
+++ /dev/null
@@ -1,212 +0,0 @@
-{
- "actions": [],
- "creation": "2013-06-19 15:57:32",
- "description": "Default settings for Shopping Cart",
- "doctype": "DocType",
- "document_type": "System",
- "engine": "InnoDB",
- "field_order": [
- "enabled",
- "store_page_docs",
- "display_settings",
- "show_attachments",
- "show_price",
- "show_stock_availability",
- "enable_variants",
- "column_break_7",
- "show_contact_us_button",
- "show_quantity_in_website",
- "show_apply_coupon_code_in_website",
- "allow_items_not_in_stock",
- "section_break_2",
- "company",
- "price_list",
- "column_break_4",
- "default_customer_group",
- "quotation_series",
- "section_break_8",
- "enable_checkout",
- "save_quotations_as_draft",
- "column_break_11",
- "payment_gateway_account",
- "payment_success_url"
- ],
- "fields": [
- {
- "default": "0",
- "fieldname": "enabled",
- "fieldtype": "Check",
- "in_list_view": 1,
- "label": "Enable Shopping Cart"
- },
- {
- "fieldname": "display_settings",
- "fieldtype": "Section Break",
- "label": "Display Settings"
- },
- {
- "default": "0",
- "fieldname": "show_attachments",
- "fieldtype": "Check",
- "label": "Show Public Attachments"
- },
- {
- "default": "0",
- "fieldname": "show_price",
- "fieldtype": "Check",
- "label": "Show Price"
- },
- {
- "default": "0",
- "fieldname": "show_stock_availability",
- "fieldtype": "Check",
- "label": "Show Stock Availability"
- },
- {
- "default": "0",
- "fieldname": "show_contact_us_button",
- "fieldtype": "Check",
- "label": "Show Contact Us Button"
- },
- {
- "default": "0",
- "depends_on": "show_stock_availability",
- "fieldname": "show_quantity_in_website",
- "fieldtype": "Check",
- "label": "Show Stock Quantity"
- },
- {
- "default": "0",
- "fieldname": "show_apply_coupon_code_in_website",
- "fieldtype": "Check",
- "label": "Show Apply Coupon Code"
- },
- {
- "default": "0",
- "fieldname": "allow_items_not_in_stock",
- "fieldtype": "Check",
- "label": "Allow items not in stock to be added to cart"
- },
- {
- "depends_on": "enabled",
- "fieldname": "section_break_2",
- "fieldtype": "Section Break"
- },
- {
- "fieldname": "company",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Company",
- "mandatory_depends_on": "eval: doc.enabled === 1",
- "options": "Company",
- "remember_last_selected_value": 1
- },
- {
- "description": "Prices will not be shown if Price List is not set",
- "fieldname": "price_list",
- "fieldtype": "Link",
- "label": "Price List",
- "mandatory_depends_on": "eval: doc.enabled === 1",
- "options": "Price List"
- },
- {
- "fieldname": "column_break_4",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "default_customer_group",
- "fieldtype": "Link",
- "ignore_user_permissions": 1,
- "label": "Default Customer Group",
- "mandatory_depends_on": "eval: doc.enabled === 1",
- "options": "Customer Group"
- },
- {
- "fieldname": "quotation_series",
- "fieldtype": "Select",
- "label": "Quotation Series",
- "mandatory_depends_on": "eval: doc.enabled === 1"
- },
- {
- "collapsible": 1,
- "collapsible_depends_on": "eval:doc.enable_checkout",
- "depends_on": "enabled",
- "fieldname": "section_break_8",
- "fieldtype": "Section Break",
- "label": "Checkout Settings"
- },
- {
- "default": "0",
- "fieldname": "enable_checkout",
- "fieldtype": "Check",
- "label": "Enable Checkout"
- },
- {
- "default": "Orders",
- "depends_on": "enable_checkout",
- "description": "After payment completion redirect user to selected page.",
- "fieldname": "payment_success_url",
- "fieldtype": "Select",
- "label": "Payment Success Url",
- "mandatory_depends_on": "enable_checkout",
- "options": "\nOrders\nInvoices\nMy Account"
- },
- {
- "fieldname": "column_break_11",
- "fieldtype": "Column Break"
- },
- {
- "depends_on": "enable_checkout",
- "fieldname": "payment_gateway_account",
- "fieldtype": "Link",
- "label": "Payment Gateway Account",
- "mandatory_depends_on": "enable_checkout",
- "options": "Payment Gateway Account"
- },
- {
- "fieldname": "column_break_7",
- "fieldtype": "Column Break"
- },
- {
- "default": "0",
- "fieldname": "enable_variants",
- "fieldtype": "Check",
- "label": "Enable Variants"
- },
- {
- "default": "0",
- "depends_on": "eval: doc.enable_checkout == 0",
- "fieldname": "save_quotations_as_draft",
- "fieldtype": "Check",
- "label": "Save Quotations as Draft"
- },
- {
- "depends_on": "doc.enabled",
- "fieldname": "store_page_docs",
- "fieldtype": "HTML"
- }
- ],
- "icon": "fa fa-shopping-cart",
- "idx": 1,
- "issingle": 1,
- "links": [],
- "modified": "2021-03-02 17:34:57.642565",
- "modified_by": "Administrator",
- "module": "Shopping Cart",
- "name": "Shopping Cart Settings",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "Website Manager",
- "share": 1,
- "write": 1
- }
- ],
- "sort_field": "modified",
- "sort_order": "ASC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py
deleted file mode 100644
index 8f4afda..0000000
--- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py
+++ /dev/null
@@ -1,85 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-
-import frappe
-from frappe import _
-from frappe.model.document import Document
-from frappe.utils import flt
-
-
-class ShoppingCartSetupError(frappe.ValidationError): pass
-
-class ShoppingCartSettings(Document):
- def onload(self):
- self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series")
-
- def validate(self):
- if self.enabled:
- self.validate_price_list_exchange_rate()
-
- def validate_price_list_exchange_rate(self):
- "Check if exchange rate exists for Price List currency (to Company's currency)."
- from erpnext.setup.utils import get_exchange_rate
-
- if not self.enabled or not self.company or not self.price_list:
- return # this function is also called from hooks, check values again
-
- company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
- price_list_currency = frappe.db.get_value("Price List", self.price_list, "currency")
-
- if not company_currency:
- msg = f"Please specify currency in Company {self.company}"
- frappe.throw(_(msg), title=_("Missing Currency"), exc=ShoppingCartSetupError)
-
- if not price_list_currency:
- msg = f"Please specify currency in Price List {frappe.bold(self.price_list)}"
- frappe.throw(_(msg), title=_("Missing Currency"), exc=ShoppingCartSetupError)
-
- if price_list_currency != company_currency:
- from_currency, to_currency = price_list_currency, company_currency
-
- # Get exchange rate checks Currency Exchange Records too
- exchange_rate = get_exchange_rate(from_currency, to_currency, args="for_selling")
-
- if not flt(exchange_rate):
- msg = f"Missing Currency Exchange Rates for {from_currency}-{to_currency}"
- frappe.throw(_(msg), title=_("Missing"), exc=ShoppingCartSetupError)
-
- def validate_tax_rule(self):
- if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart" : 1}, "name"):
- frappe.throw(frappe._("Set Tax Rule for shopping cart"), ShoppingCartSetupError)
-
- def get_tax_master(self, billing_territory):
- tax_master = self.get_name_from_territory(billing_territory, "sales_taxes_and_charges_masters",
- "sales_taxes_and_charges_master")
- return tax_master and tax_master[0] or None
-
- def get_shipping_rules(self, shipping_territory):
- return self.get_name_from_territory(shipping_territory, "shipping_rules", "shipping_rule")
-
-def validate_cart_settings(doc=None, method=None):
- frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings").run_method("validate")
-
-def get_shopping_cart_settings():
- if not getattr(frappe.local, "shopping_cart_settings", None):
- frappe.local.shopping_cart_settings = frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings")
-
- return frappe.local.shopping_cart_settings
-
-@frappe.whitelist(allow_guest=True)
-def is_cart_enabled():
- return get_shopping_cart_settings().enabled
-
-def show_quantity_in_website():
- return get_shopping_cart_settings().show_quantity_in_website
-
-def check_shopping_cart_enabled():
- if not get_shopping_cart_settings().enabled:
- frappe.throw(_("You need to enable Shopping Cart"), ShoppingCartSetupError)
-
-def show_attachments():
- return get_shopping_cart_settings().show_attachments
diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py b/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py
deleted file mode 100644
index 1164a5d..0000000
--- a/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-
-import unittest
-
-import frappe
-
-from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
- ShoppingCartSetupError,
-)
-
-
-class TestShoppingCartSettings(unittest.TestCase):
- def setUp(self):
- frappe.db.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """)
-
- def get_cart_settings(self):
- return frappe.get_doc({"doctype": "Shopping Cart Settings",
- "company": "_Test Company"})
-
- # NOTE: Exchangrate API has all enabled currencies that ERPNext supports.
- # We aren't checking just currency exchange record anymore
- # while validating price list currency exchange rate to that of company.
- # The API is being used to fetch the rate which again almost always
- # gives back a valid value (for valid currencies).
- # This makes the test obsolete.
- # Commenting because im not sure if there's a better test we can write
-
- # def test_exchange_rate_exists(self):
- # frappe.db.sql("""delete from `tabCurrency Exchange`""")
-
- # cart_settings = self.get_cart_settings()
- # cart_settings.price_list = "_Test Price List Rest of the World"
- # self.assertRaises(ShoppingCartSetupError, cart_settings.validate_price_list_exchange_rate)
-
- # from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \
- # currency_exchange_records
- # frappe.get_doc(currency_exchange_records[0]).insert()
- # cart_settings.validate_price_list_exchange_rate()
-
- def test_tax_rule_validation(self):
- frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
-
- cart_settings = self.get_cart_settings()
- cart_settings.enabled = 1
- if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"):
- self.assertRaises(ShoppingCartSetupError, cart_settings.validate_tax_rule)
-
- frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1")
-
-test_dependencies = ["Tax Rule"]
diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py
deleted file mode 100644
index 5cc0505..0000000
--- a/erpnext/shopping_cart/product_query.py
+++ /dev/null
@@ -1,161 +0,0 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-import frappe
-
-from erpnext.shopping_cart.product_info import get_product_info_for_website
-
-
-class ProductQuery:
- """Query engine for product listing
-
- Attributes:
- cart_settings (Document): Settings for Cart
- fields (list): Fields to fetch in query
- filters (TYPE): Description
- or_filters (list): Description
- page_length (Int): Length of page for the query
- settings (Document): Products Settings DocType
- filters (list)
- or_filters (list)
- """
-
- def __init__(self):
- self.settings = frappe.get_doc("Products Settings")
- self.cart_settings = frappe.get_doc("Shopping Cart Settings")
- self.page_length = self.settings.products_per_page or 20
- self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants',
- 'item_group', 'image', 'web_long_description', 'description', 'route', 'weightage']
- self.filters = []
- self.or_filters = [['show_in_website', '=', 1]]
- if not self.settings.get('hide_variants'):
- self.or_filters.append(['show_variant_in_website', '=', 1])
-
- def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None):
- """Summary
-
- Args:
- attributes (dict, optional): Item Attribute filters
- fields (dict, optional): Field level filters
- search_term (str, optional): Search term to lookup
- start (int, optional): Page start
-
- Returns:
- list: List of results with set fields
- """
- if fields: self.build_fields_filters(fields)
- if search_term: self.build_search_filters(search_term)
-
- result = []
- website_item_groups = []
-
- # if from item group page consider website item group table
- if item_group:
- website_item_groups = frappe.db.get_all(
- "Item",
- fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
- filters=[["Website Item Group", "item_group", "=", item_group]]
- )
-
- if attributes:
- all_items = []
- for attribute, values in attributes.items():
- if not isinstance(values, list):
- values = [values]
-
- items = frappe.get_all(
- "Item",
- fields=self.fields,
- filters=[
- *self.filters,
- ["Item Variant Attribute", "attribute", "=", attribute],
- ["Item Variant Attribute", "attribute_value", "in", values],
- ],
- or_filters=self.or_filters,
- start=start,
- limit=self.page_length,
- order_by="weightage desc"
- )
-
- items_dict = {item.name: item for item in items}
-
- all_items.append(set(items_dict.keys()))
-
- result = [items_dict.get(item) for item in list(set.intersection(*all_items))]
- else:
- result = frappe.get_all(
- "Item",
- fields=self.fields,
- filters=self.filters,
- or_filters=self.or_filters,
- start=start,
- limit=self.page_length,
- order_by="weightage desc"
- )
-
- # Combine results having context of website item groups into item results
- if item_group and website_item_groups:
- items_list = {row.name for row in result}
- for row in website_item_groups:
- if row.wig_parent not in items_list:
- result.append(row)
-
- result = sorted(result, key=lambda x: x.get("weightage"), reverse=True)
-
- for item in result:
- product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
- if product_info:
- item.formatted_price = (product_info.get('price') or {}).get('formatted_price')
-
- return result
-
- def build_fields_filters(self, filters):
- """Build filters for field values
-
- Args:
- filters (dict): Filters
- """
- for field, values in filters.items():
- if not values:
- continue
-
- # handle multiselect fields in filter addition
- meta = frappe.get_meta('Item', cached=True)
- df = meta.get_field(field)
- if df.fieldtype == 'Table MultiSelect':
- child_doctype = df.options
- child_meta = frappe.get_meta(child_doctype, cached=True)
- fields = child_meta.get("fields")
- if fields:
- self.filters.append([child_doctype, fields[0].fieldname, 'IN', values])
- elif isinstance(values, list):
- # If value is a list use `IN` query
- self.filters.append([field, 'IN', values])
- else:
- # `=` will be faster than `IN` for most cases
- self.filters.append([field, '=', values])
-
- def build_search_filters(self, search_term):
- """Query search term in specified fields
-
- Args:
- search_term (str): Search candidate
- """
- # Default fields to search from
- default_fields = {'name', 'item_name', 'description', 'item_group'}
-
- # Get meta search fields
- meta = frappe.get_meta("Item")
- meta_fields = set(meta.get_search_fields())
-
- # Join the meta fields and default fields set
- search_fields = default_fields.union(meta_fields)
- try:
- if frappe.db.count('Item', cache=True) > 50000:
- search_fields.remove('description')
- except KeyError:
- pass
-
- # Build or filters for query
- search = '%{}%'.format(search_term)
- self.or_filters += [[field, 'like', search] for field in search_fields]
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 752a1fe..f361c88 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -17,8 +17,6 @@
frm.fields_dict["attributes"].grid.set_column_disp("attribute_value", true);
}
- // should never check Private
- frm.fields_dict["website_image"].df.is_private = 0;
if (frm.doc.is_fixed_asset) {
frm.trigger("set_asset_naming_series");
}
@@ -91,6 +89,29 @@
erpnext.toggle_naming_series();
}
+ if (!frm.doc.published_in_website) {
+ frm.add_custom_button(__("Publish in Website"), function() {
+ frappe.call({
+ method: "erpnext.e_commerce.doctype.website_item.website_item.make_website_item",
+ args: {doc: frm.doc},
+ freeze: true,
+ freeze_message: __("Publishing Item ..."),
+ callback: function(result) {
+ frappe.msgprint({
+ message: __("Website Item {0} has been created.",
+ [repl('<a href="/app/website-item/%(item_encoded)s" class="strong">%(item)s</a>', {
+ item_encoded: encodeURIComponent(result.message[0]),
+ item: result.message[1]
+ })]
+ ),
+ title: __("Published"),
+ indicator: "green"
+ });
+ }
+ });
+ }, __('Actions'));
+ }
+
erpnext.item.edit_prices_button(frm);
erpnext.item.toggle_attributes(frm);
@@ -182,25 +203,8 @@
}
},
- copy_from_item_group: function(frm) {
- return frm.call({
- doc: frm.doc,
- method: "copy_specification_from_item_group"
- });
- },
-
has_variants: function(frm) {
erpnext.item.toggle_attributes(frm);
- },
-
- show_in_website: function(frm) {
- if (frm.doc.default_warehouse && !frm.doc.website_warehouse){
- frm.set_value("website_warehouse", frm.doc.default_warehouse);
- }
- },
-
- set_meta_tags(frm) {
- frappe.utils.set_meta_tag(frm.doc.route);
}
});
@@ -393,13 +397,15 @@
edit_prices_button: function(frm) {
frm.add_custom_button(__("Add / Edit Prices"), function() {
frappe.set_route("List", "Item Price", {"item_code": frm.doc.name});
- }, __("View"));
+ }, __("Actions"));
},
- weight_to_validate: function(frm){
- if((frm.doc.nett_weight || frm.doc.gross_weight) && !frm.doc.weight_uom) {
- frappe.msgprint(__('Weight is mentioned,\nPlease mention "Weight UOM" too'));
- frappe.validated = 0;
+ weight_to_validate: function(frm) {
+ if (frm.doc.weight_per_unit && !frm.doc.weight_uom) {
+ frappe.msgprint({
+ message: __("Please mention 'Weight UOM' along with Weight."),
+ title: __("Note")
+ });
}
},
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index db5caf9..36b3075 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -116,30 +116,14 @@
"customer_code",
"default_item_manufacturer",
"default_manufacturer_part_no",
- "website_section",
- "show_in_website",
- "show_variant_in_website",
- "route",
- "weightage",
- "slideshow",
- "website_image",
- "website_image_alt",
- "thumbnail",
- "cb72",
- "website_warehouse",
- "website_item_groups",
- "set_meta_tags",
- "sb72",
- "copy_from_item_group",
- "website_specifications",
- "web_long_description",
- "website_content",
- "total_projected_qty",
"hub_publishing_sb",
"publish_in_hub",
"hub_category_to_publish",
"hub_warehouse",
- "synced_with_hub"
+ "synced_with_hub",
+ "more_information_section",
+ "published_in_website",
+ "total_projected_qty"
],
"fields": [
{
@@ -870,125 +854,6 @@
"print_hide": 1
},
{
- "collapsible": 1,
- "depends_on": "eval:!doc.is_fixed_asset",
- "fieldname": "website_section",
- "fieldtype": "Section Break",
- "label": "Website",
- "options": "fa fa-globe"
- },
- {
- "default": "0",
- "depends_on": "eval:!doc.variant_of",
- "fieldname": "show_in_website",
- "fieldtype": "Check",
- "label": "Show in Website",
- "search_index": 1
- },
- {
- "default": "0",
- "depends_on": "variant_of",
- "fieldname": "show_variant_in_website",
- "fieldtype": "Check",
- "label": "Show in Website (Variant)",
- "search_index": 1
- },
- {
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "fieldname": "route",
- "fieldtype": "Small Text",
- "label": "Route",
- "no_copy": 1
- },
- {
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "description": "Items with higher weightage will be shown higher",
- "fieldname": "weightage",
- "fieldtype": "Int",
- "label": "Weightage"
- },
- {
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "description": "Show a slideshow at the top of the page",
- "fieldname": "slideshow",
- "fieldtype": "Link",
- "label": "Slideshow",
- "options": "Website Slideshow"
- },
- {
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "description": "Item Image (if not slideshow)",
- "fieldname": "website_image",
- "fieldtype": "Attach",
- "label": "Website Image"
- },
- {
- "fieldname": "thumbnail",
- "fieldtype": "Data",
- "label": "Thumbnail",
- "read_only": 1
- },
- {
- "fieldname": "cb72",
- "fieldtype": "Column Break"
- },
- {
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "description": "Show \"In Stock\" or \"Not in Stock\" based on stock available in this warehouse.",
- "fieldname": "website_warehouse",
- "fieldtype": "Link",
- "ignore_user_permissions": 1,
- "label": "Website Warehouse",
- "options": "Warehouse"
- },
- {
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "description": "List this Item in multiple groups on the website.",
- "fieldname": "website_item_groups",
- "fieldtype": "Table",
- "label": "Website Item Groups",
- "options": "Website Item Group"
- },
- {
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "fieldname": "set_meta_tags",
- "fieldtype": "Button",
- "label": "Set Meta Tags"
- },
- {
- "collapsible": 1,
- "collapsible_depends_on": "website_specifications",
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "fieldname": "sb72",
- "fieldtype": "Section Break",
- "label": "Website Specifications"
- },
- {
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "fieldname": "copy_from_item_group",
- "fieldtype": "Button",
- "label": "Copy From Item Group"
- },
- {
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "fieldname": "website_specifications",
- "fieldtype": "Table",
- "label": "Website Specifications",
- "options": "Item Website Specification"
- },
- {
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "fieldname": "web_long_description",
- "fieldtype": "Text Editor",
- "label": "Website Description"
- },
- {
- "description": "You can use any valid Bootstrap 4 markup in this field. It will be shown on your Item Page.",
- "fieldname": "website_content",
- "fieldtype": "HTML Editor",
- "label": "Website Content"
- },
- {
"fieldname": "total_projected_qty",
"fieldtype": "Float",
"hidden": 1,
@@ -1065,20 +930,27 @@
"read_only": 1
},
{
- "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
- "fieldname": "website_image_alt",
- "fieldtype": "Data",
- "label": "Image Description"
+ "collapsible": 1,
+ "fieldname": "more_information_section",
+ "fieldtype": "Section Break",
+ "label": "More Information"
+ },
+ {
+ "default": "0",
+ "depends_on": "published_in_website",
+ "fieldname": "published_in_website",
+ "fieldtype": "Check",
+ "label": "Published in Website",
+ "read_only": 1
}
],
- "has_web_view": 1,
"icon": "fa fa-tag",
"idx": 2,
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 1,
- "modified": "2021-08-26 12:23:07.277077",
+ "modified": "2021-10-12 12:23:07.277077",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 8cc9f74..0e9e631 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -21,9 +21,8 @@
strip,
)
from frappe.utils.html_utils import clean_html
-from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow
from frappe.website.utils import clear_cache
-from frappe.website.website_generator import WebsiteGenerator
+from frappe.model.document import Document
import erpnext
from erpnext.controllers.item_variant import (
@@ -52,17 +51,8 @@
pass
-class Item(WebsiteGenerator):
- website = frappe._dict(
- page_title_field="item_name",
- condition_field="show_in_website",
- template="templates/generators/item/item.html",
- no_cache=1
- )
-
+class Item(Document):
def onload(self):
- super(Item, self).onload()
-
self.set_onload('stock_exists', self.stock_ledger_created())
self.set_asset_naming_series()
@@ -103,8 +93,6 @@
self.set_opening_stock()
def validate(self):
- super(Item, self).validate()
-
if not self.item_name:
self.item_name = self.item_code
@@ -131,8 +119,6 @@
self.validate_attributes()
self.validate_variant_attributes()
self.validate_variant_based_on_change()
- self.validate_website_image()
- self.make_thumbnail()
self.validate_fixed_asset()
self.validate_retain_sample()
self.validate_uom_conversion_factor()
@@ -141,22 +127,17 @@
self.validate_item_defaults()
self.validate_auto_reorder_enabled_in_stock_settings()
self.cant_change()
- self.update_show_in_website()
self.validate_item_tax_net_rate_range()
set_item_tax_from_hsn_code(self)
if not self.is_new():
self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group")
- self.old_website_item_groups = frappe.db.sql_list("""select item_group
- from `tabWebsite Item Group`
- where parentfield='website_item_groups' and parenttype='Item' and parent=%s""", self.name)
def on_update(self):
invalidate_cache_for_item(self)
self.validate_name_with_item_group()
self.update_variants()
self.update_item_price()
- self.update_template_item()
def validate_description(self):
'''Clean HTML description if set'''
@@ -218,95 +199,6 @@
stock_entry.add_comment("Comment", _("Opening Stock"))
- def make_route(self):
- if not self.route:
- return cstr(frappe.db.get_value('Item Group', self.item_group,
- 'route')) + '/' + self.scrub((self.item_name or self.item_code) + '-' + random_string(5))
-
- def validate_website_image(self):
- if frappe.flags.in_import:
- return
-
- """Validate if the website image is a public file"""
- auto_set_website_image = False
- if not self.website_image and self.image:
- auto_set_website_image = True
- self.website_image = self.image
-
- if not self.website_image:
- return
-
- # find if website image url exists as public
- file_doc = frappe.get_all("File", filters={
- "file_url": self.website_image
- }, fields=["name", "is_private"], order_by="is_private asc", limit_page_length=1)
-
- if file_doc:
- file_doc = file_doc[0]
-
- if not file_doc:
- if not auto_set_website_image:
- frappe.msgprint(_("Website Image {0} attached to Item {1} cannot be found").format(self.website_image, self.name))
-
- self.website_image = None
-
- elif file_doc.is_private:
- if not auto_set_website_image:
- frappe.msgprint(_("Website Image should be a public file or website URL"))
-
- self.website_image = None
-
- def make_thumbnail(self):
- if frappe.flags.in_import:
- return
-
- """Make a thumbnail of `website_image`"""
- import requests.exceptions
-
- if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"):
- self.thumbnail = None
-
- if self.website_image and not self.thumbnail:
- file_doc = None
-
- try:
- file_doc = frappe.get_doc("File", {
- "file_url": self.website_image,
- "attached_to_doctype": "Item",
- "attached_to_name": self.name
- })
- except frappe.DoesNotExistError:
- # cleanup
- frappe.local.message_log.pop()
-
- except requests.exceptions.HTTPError:
- frappe.msgprint(_("Warning: Invalid attachment {0}").format(self.website_image))
- self.website_image = None
-
- except requests.exceptions.SSLError:
- frappe.msgprint(
- _("Warning: Invalid SSL certificate on attachment {0}").format(self.website_image))
- self.website_image = None
-
- # for CSV import
- if self.website_image and not file_doc:
- try:
- file_doc = frappe.get_doc({
- "doctype": "File",
- "file_url": self.website_image,
- "attached_to_doctype": "Item",
- "attached_to_name": self.name
- }).save()
-
- except IOError:
- self.website_image = None
-
- if file_doc:
- if not file_doc.thumbnail_url:
- file_doc.make_thumbnail()
-
- self.thumbnail = file_doc.thumbnail_url
-
def validate_fixed_asset(self):
if self.is_fixed_asset:
if self.is_stock_item:
@@ -330,167 +222,6 @@
frappe.throw(_("{0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format(
self.item_code))
- def get_context(self, context):
- context.show_search = True
- context.search_link = '/product_search'
-
- context.parents = get_parent_item_groups(self.item_group)
- context.body_class = "product-page"
-
- self.set_variant_context(context)
- self.set_attribute_context(context)
- self.set_disabled_attributes(context)
- self.set_metatags(context)
- self.set_shopping_cart_data(context)
-
- return context
-
- def set_variant_context(self, context):
- if self.has_variants:
- context.no_cache = True
-
- # load variants
- # also used in set_attribute_context
- context.variants = frappe.get_all("Item",
- filters={"variant_of": self.name, "show_variant_in_website": 1},
- order_by="name asc")
-
- variant = frappe.form_dict.variant
- if not variant and context.variants:
- # the case when the item is opened for the first time from its list
- variant = context.variants[0]
-
- if variant:
- context.variant = frappe.get_doc("Item", variant)
-
- for fieldname in ("website_image", "website_image_alt", "web_long_description", "description",
- "website_specifications"):
- 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:
- if context.variant and context.variant.slideshow:
- context.update(get_slideshow(context.variant))
- else:
- context.update(get_slideshow(self))
-
- def set_attribute_context(self, context):
- if not self.has_variants:
- return
-
- attribute_values_available = {}
- context.attribute_values = {}
- context.selected_attributes = {}
-
- # load attributes
- for v in context.variants:
- v.attributes = frappe.get_all("Item Variant Attribute",
- fields=["attribute", "attribute_value"],
- filters={"parent": v.name})
- # make a map for easier access in templates
- v.attribute_map = frappe._dict({})
- for attr in v.attributes:
- v.attribute_map[attr.attribute] = attr.attribute_value
-
- for attr in v.attributes:
- values = attribute_values_available.setdefault(attr.attribute, [])
- if attr.attribute_value not in values:
- values.append(attr.attribute_value)
-
- if v.name == context.variant.name:
- context.selected_attributes[attr.attribute] = attr.attribute_value
-
- # filter attributes, order based on attribute table
- for attr in self.attributes:
- values = context.attribute_values.setdefault(attr.attribute, [])
-
- 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)
-
- 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 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):
- context.metatags = frappe._dict({})
-
- safe_description = frappe.utils.to_markdown(self.description)
-
- context.metatags.url = frappe.utils.get_url() + '/' + context.route
-
- if context.website_image:
- if context.website_image.startswith('http'):
- url = context.website_image
- else:
- url = frappe.utils.get_url() + context.website_image
- context.metatags.image = url
-
- context.metatags.description = safe_description[:300]
-
- context.metatags.title = self.item_name or self.item_code
-
- context.metatags['og:type'] = 'product'
- context.metatags['og:site_name'] = 'ERPNext'
-
- def set_shopping_cart_data(self, context):
- from erpnext.shopping_cart.product_info import get_product_info_for_website
- context.shopping_cart = get_product_info_for_website(self.name, skip_quotation_creation=True)
-
def add_default_uom_in_conversion_factor_table(self):
uom_conv_list = [d.uom for d in self.get("uoms")]
if self.stock_uom not in uom_conv_list:
@@ -505,10 +236,6 @@
[self.remove(d) for d in to_remove]
- def update_show_in_website(self):
- if self.disabled:
- self.show_in_website = False
-
def validate_item_tax_net_rate_range(self):
for tax in self.get('taxes'):
if flt(tax.maximum_net_rate) < flt(tax.minimum_net_rate):
@@ -653,7 +380,6 @@
)
def on_trash(self):
- super(Item, self).on_trash()
frappe.db.sql("""delete from tabBin where item_code=%s""", self.name)
frappe.db.sql("delete from `tabItem Price` where item_code=%s", self.name)
for variant_of in frappe.get_all("Item", filters={"variant_of": self.name}):
@@ -678,9 +404,8 @@
if merge:
self.validate_duplicate_item_in_stock_reconciliation(old_name, new_name)
- if self.route:
+ if self.published_in_website:
invalidate_cache_for_item(self)
- clear_cache(self.route)
frappe.db.set_value("Item", new_name, "item_code", new_name)
@@ -744,16 +469,6 @@
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
frappe.db.auto_commit_on_many_writes = 0
- @frappe.whitelist()
- def copy_specification_from_item_group(self):
- self.set("website_specifications", [])
- if self.item_group:
- for label, desc in frappe.db.get_values("Item Website Specification",
- {"parent": self.item_group}, ["label", "description"]):
- row = self.append("website_specifications")
- row.label = label
- row.description = desc
-
def update_bom_item_desc(self):
if self.is_new():
return
@@ -777,25 +492,6 @@
where item_code = %s and docstatus < 2
""", (self.description, self.name))
- def update_template_item(self):
- """Set Show in Website for Template Item if True for its Variant"""
- if not self.variant_of:
- return
-
- if self.show_in_website:
- self.show_variant_in_website = 1
- self.show_in_website = 0
-
- if self.show_variant_in_website:
- # show template
- template_item = frappe.get_doc("Item", self.variant_of)
-
- if not template_item.show_in_website:
- template_item.show_in_website = 1
- template_item.flags.dont_update_variants = True
- template_item.flags.ignore_permissions = True
- template_item.save()
-
def validate_item_defaults(self):
companies = {row.company for row in self.item_defaults}
@@ -1046,47 +742,6 @@
if not enabled:
frappe.msgprint(msg=_("You have to enable auto re-order in Stock Settings to maintain re-order levels."), title=_("Enable Auto Re-Order"), indicator="orange")
- def create_onboarding_docs(self, args):
- company = frappe.defaults.get_defaults().get('company') or \
- frappe.db.get_single_value('Global Defaults', 'default_company')
-
- for i in range(1, args.get('max_count')):
- item = args.get('item_' + str(i))
- if item:
- default_warehouse = ''
- default_warehouse = frappe.db.get_value('Warehouse', filters={
- 'warehouse_name': _('Finished Goods'),
- 'company': company
- })
-
- try:
- frappe.get_doc({
- 'doctype': self.doctype,
- 'item_code': item,
- 'item_name': item,
- 'description': item,
- 'show_in_website': 1,
- 'is_sales_item': 1,
- 'is_purchase_item': 1,
- 'is_stock_item': 1,
- 'item_group': _('Products'),
- 'stock_uom': _(args.get('item_uom_' + str(i))),
- 'item_defaults': [{
- 'default_warehouse': default_warehouse,
- 'company': company
- }]
- }).insert()
-
- except frappe.NameError:
- pass
- else:
- if args.get('item_price_' + str(i)):
- item_price = flt(args.get('item_price_' + str(i)))
-
- price_list_name = frappe.db.get_value('Price List', {'selling': 1})
- make_item_price(item, price_list_name, item_price)
- price_list_name = frappe.db.get_value('Price List', {'buying': 1})
- make_item_price(item, price_list_name, item_price)
def make_item_price(item, price_list_name, item_price):
frappe.get_doc({
@@ -1201,14 +856,9 @@
def invalidate_cache_for_item(doc):
+ """Invalidate Item Group cache and rebuild ItemVariantsCacheManager."""
invalidate_cache_for(doc, doc.item_group)
- website_item_groups = list(set((doc.get("old_website_item_groups") or [])
- + [d.item_group for d in doc.get({"doctype": "Website Item Group"}) if d.item_group]))
-
- for item_group in website_item_groups:
- invalidate_cache_for(doc, item_group)
-
if doc.get("old_item_group") and doc.get("old_item_group") != doc.item_group:
invalidate_cache_for(doc, doc.old_item_group)
@@ -1216,12 +866,14 @@
def invalidate_item_variants_cache_for_website(doc):
- from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager
+ """Rebuild ItemVariantsCacheManager via Item or Website Item."""
+ from erpnext.e_commerce.product_configurator.item_variants_cache import ItemVariantsCacheManager
item_code = None
- if doc.has_variants and doc.show_in_website:
- item_code = doc.name
- elif doc.variant_of and frappe.db.get_value('Item', doc.variant_of, 'show_in_website'):
+ is_web_item = doc.get("published_in_website") or doc.get("published")
+ if doc.has_variants and is_web_item:
+ item_code = doc.item_code
+ elif doc.variant_of and frappe.db.get_value('Item', doc.variant_of, 'published_in_website'):
item_code = doc.variant_of
if item_code:
@@ -1345,10 +997,6 @@
if publish_progress:
frappe.publish_progress(count / total * 100, title=_("Updating Variants..."))
-def on_doctype_update():
- # since route is a Text column, it needs a length for indexing
- frappe.db.add_index("Item", ["route(500)"])
-
@erpnext.allow_regional
def set_item_tax_from_hsn_code(item):
pass
diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json
index 6cec852..91c77d5 100644
--- a/erpnext/stock/doctype/item/test_records.json
+++ b/erpnext/stock/doctype/item/test_records.json
@@ -40,9 +40,7 @@
"conversion_factor": 10.0
}
],
- "stock_uom": "_Test UOM",
- "show_in_website": 1,
- "website_warehouse": "_Test Warehouse - _TC"
+ "stock_uom": "_Test UOM"
},
{
"description": "_Test Item 2",
@@ -56,8 +54,6 @@
"item_group": "_Test Item Group",
"item_name": "_Test Item 2",
"stock_uom": "_Test UOM",
- "show_in_website": 1,
- "website_warehouse": "_Test Warehouse - _TC",
"gst_hsn_code": "999800",
"opening_stock": 10,
"valuation_rate": 100,
@@ -311,8 +307,7 @@
"warehouse_reorder_level": 20,
"warehouse_reorder_qty": 20
}
- ],
- "show_in_website": 1
+ ]
},
{
"description": "_Test Item 1",
@@ -344,9 +339,7 @@
"warehouse_reorder_qty": 20
}
],
- "stock_uom": "_Test UOM",
- "show_in_website": 1,
- "website_warehouse": "_Test Warehouse Group-C1 - _TC"
+ "stock_uom": "_Test UOM"
},
{
"description": "_Test Item With Item Tax Template",
diff --git a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js
index 488920a..5e1f7d5 100644
--- a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js
+++ b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.js
@@ -7,9 +7,8 @@
const existing_fields = frm.doc.fields.map(row => row.field_name);
const exclude_fields = [...existing_fields, "naming_series", "item_code", "item_name",
- "show_in_website", "show_variant_in_website", "standard_rate", "opening_stock", "image",
- "variant_of", "valuation_rate", "barcodes", "website_image", "thumbnail",
- "website_specifiations", "web_long_description", "has_variants", "attributes"];
+ "published_in_website", "standard_rate", "opening_stock", "image",
+ "variant_of", "valuation_rate", "barcodes", "has_variants", "attributes"];
const exclude_field_types = ['HTML', 'Section Break', 'Column Break', 'Button', 'Read Only'];
diff --git a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py
index cb6626f..115bdaa 100644
--- a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py
+++ b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py
@@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
-
import frappe
from frappe import _
from frappe.model.document import Document
@@ -15,10 +13,9 @@
def set_default_fields(self):
self.fields = []
fields = frappe.get_meta('Item').fields
- exclude_fields = {"naming_series", "item_code", "item_name", "show_in_website",
- "show_variant_in_website", "standard_rate", "opening_stock", "image", "description",
+ exclude_fields = {"naming_series", "item_code", "item_name", "published_in_website",
+ "standard_rate", "opening_stock", "image", "description",
"variant_of", "valuation_rate", "description", "barcodes",
- "website_image", "thumbnail", "website_specifiations", "web_long_description",
"has_variants", "attributes"}
for d in fields:
diff --git a/erpnext/stock/onboarding_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json b/erpnext/stock/onboarding_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json
deleted file mode 100644
index 5ee3167..0000000
--- a/erpnext/stock/onboarding_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json
+++ /dev/null
@@ -1,52 +0,0 @@
-{
- "add_more_button": 1,
- "app": "ERPNext",
- "creation": "2019-11-15 14:41:12.007359",
- "docstatus": 0,
- "doctype": "Onboarding Slide",
- "domains": [],
- "help_links": [],
- "idx": 0,
- "image_src": "",
- "is_completed": 0,
- "max_count": 3,
- "modified": "2019-12-09 17:54:09.602885",
- "modified_by": "Administrator",
- "name": "Add A Few Products You Buy Or Sell",
- "owner": "Administrator",
- "ref_doctype": "Item",
- "slide_desc": "",
- "slide_fields": [
- {
- "align": "",
- "fieldname": "item",
- "fieldtype": "Data",
- "label": "Item",
- "placeholder": "Product Name",
- "reqd": 1
- },
- {
- "align": "",
- "fieldname": "item_price",
- "fieldtype": "Currency",
- "label": "Item Price",
- "reqd": 1
- },
- {
- "align": "",
- "fieldtype": "Column Break",
- "reqd": 0
- },
- {
- "align": "",
- "fieldname": "uom",
- "fieldtype": "Link",
- "label": "UOM",
- "options": "UOM",
- "reqd": 1
- }
- ],
- "slide_order": 30,
- "slide_title": "Add A Few Products You Buy Or Sell",
- "slide_type": "Create"
-}
\ No newline at end of file
diff --git a/erpnext/templates/generators/item/item.html b/erpnext/templates/generators/item/item.html
index 17f6880..427e568 100644
--- a/erpnext/templates/generators/item/item.html
+++ b/erpnext/templates/generators/item/item.html
@@ -9,18 +9,39 @@
{% endblock %}
{% block page_content %}
-<div class="product-container">
+<div class="product-container col-md-12">
{% from "erpnext/templates/includes/macros.html" import product_image %}
<div class="item-content">
<div class="product-page-content" itemscope itemtype="http://schema.org/Product">
+ <!-- Image, Description, Add to Cart -->
<div class="row mb-5">
{% include "templates/generators/item/item_image.html" %}
{% include "templates/generators/item/item_details.html" %}
</div>
- {% include "templates/generators/item/item_specifications.html" %}
+ <!-- Product Specifications Table Section -->
+ {% if show_tabs and tabs %}
+ <div class="category-tabs">
+ <!-- tabs -->
+ {{ web_block(
+ "Section with Tabs",
+ values=tabs,
+ add_container=0,
+ add_top_padding=0,
+ add_bottom_padding=0
+ ) }}
+ </div>
+ {% elif website_specifications %}
+ {% include "templates/generators/item/item_specifications.html"%}
+ {% endif %}
+ <!-- Advanced Custom Website Content -->
{{ doc.website_content or '' }}
+
+ <!-- Reviews and Comments -->
+ {% if shopping_cart.cart_settings.enable_reviews and not doc.has_variants %}
+ {% include "templates/generators/item/item_reviews.html"%}
+ {% endif %}
</div>
</div>
</div>
diff --git a/erpnext/templates/generators/item/item_add_to_cart.html b/erpnext/templates/generators/item/item_add_to_cart.html
index 167c848..1da4d15 100644
--- a/erpnext/templates/generators/item/item_add_to_cart.html
+++ b/erpnext/templates/generators/item/item_add_to_cart.html
@@ -5,10 +5,23 @@
<div class="item-cart row mt-2" data-variant-item-code="{{ item_code }}">
<div class="col-md-12">
+ <!-- Price and Availability -->
{% if cart_settings.show_price and product_info.price %}
+ {% set price_info = product_info.price %}
+
+ {% if price_info.formatted_mrp %}
+ <small class="formatted-price">
+ M.R.P.:
+ <s>{{ price_info.formatted_mrp }}</s>
+ </small>
+ <small class="ml-2 formatted-price" style="color: #F47A7A; font-weight: 500;">
+ {{ price_info.get("formatted_discount_percent") or price_info.get("formatted_discount_rate")}} OFF
+ </small>
+ {% endif %}
+
<div class="product-price">
- {{ product_info.price.formatted_price_sales_uom }}
- <small class="formatted-price">({{ product_info.price.formatted_price }} / {{ product_info.uom }})</small>
+ {{ price_info.formatted_price_sales_uom }}
+ <small class="formatted-price">({{ price_info.formatted_price }} / {{ product_info.uom }})</small>
</div>
{% else %}
{{ _("UOM") }} : {{ product_info.uom }}
@@ -30,17 +43,50 @@
{% endif %}
</div>
{% endif %}
+
+ <!-- Offers -->
+ {% if doc.offers %}
+ <br>
+ <h3>Offers</h3>
+ <div class="offer-container">
+ {% for offer in doc.offers %}
+ <div class="mt-2" style="display: flex;">
+ <div class="mr-2" >
+ <svg width="24" height="24" viewBox="0 0 24 24" stroke="var(--yellow-500)" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M19 15.6213C19 15.2235 19.158 14.842 19.4393 14.5607L20.9393 13.0607C21.5251 12.4749 21.5251 11.5251 20.9393 10.9393L19.4393 9.43934C19.158 9.15804 19 8.7765 19 8.37868V6.5C19 5.67157 18.3284 5 17.5 5H15.6213C15.2235 5 14.842 4.84196 14.5607 4.56066L13.0607 3.06066C12.4749 2.47487 11.5251 2.47487 10.9393 3.06066L9.43934 4.56066C9.15804 4.84196 8.7765 5 8.37868 5H6.5C5.67157 5 5 5.67157 5 6.5V8.37868C5 8.7765 4.84196 9.15804 4.56066 9.43934L3.06066 10.9393C2.47487 11.5251 2.47487 12.4749 3.06066 13.0607L4.56066 14.5607C4.84196 14.842 5 15.2235 5 15.6213V17.5C5 18.3284 5.67157 19 6.5 19H8.37868C8.7765 19 9.15804 19.158 9.43934 19.4393L10.9393 20.9393C11.5251 21.5251 12.4749 21.5251 13.0607 20.9393L14.5607 19.4393C14.842 19.158 15.2235 19 15.6213 19H17.5C18.3284 19 19 18.3284 19 17.5V15.6213Z" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M15 9L9 15" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M10.5 9.5C10.5 10.0523 10.0523 10.5 9.5 10.5C8.94772 10.5 8.5 10.0523 8.5 9.5C8.5 8.94772 8.94772 8.5 9.5 8.5C10.0523 8.5 10.5 8.94772 10.5 9.5Z" fill="white" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M15.5 14.5C15.5 15.0523 15.0523 15.5 14.5 15.5C13.9477 15.5 13.5 15.0523 13.5 14.5C13.5 13.9477 13.9477 13.5 14.5 13.5C15.0523 13.5 15.5 13.9477 15.5 14.5Z" fill="white" stroke-linecap="round" stroke-linejoin="round"/>
+ </svg>
+ </div>
+ <p class="mr-1">
+ <strong>{{ _(offer.offer_title) }}:</strong>
+ {{ _(offer.offer_subtitle) }}
+ <a class="offer-details" href="#"
+ data-offer-title="{{ offer.offer_title }}" data-offer-id="{{ offer.name }}"
+ role="button">
+ {{ _("More") }}
+ </a>
+ </p>
+ </div>
+ {% endfor %}
+ </div>
+ {% endif %}
+
+ <!-- Add to Cart / View in Cart, Contact Us -->
<div class="mt-5 mb-5">
- {% if product_info.price and (cart_settings.allow_items_not_in_stock or product_info.in_stock) %}
+ <div style="display: flex;" class="mb-4">
+ <!-- Add to Cart -->
+ {% if product_info.price and (cart_settings.allow_items_not_in_stock or product_info.in_stock) %}
<a href="/cart"
- class="btn btn-light btn-view-in-cart {% if not product_info.qty %}hidden{% endif %}"
+ class="btn btn-light btn-view-in-cart hidden mr-2"
role="button"
>
{{ _("View in Cart") }}
</a>
<button
data-item-code="{{item_code}}"
- class="btn btn-primary btn-add-to-cart {% if product_info.qty %}hidden{% endif %} w-100"
+ class="btn btn-primary btn-add-to-cart w-50 mr-2"
>
<span class="mr-2">
<svg class="icon icon-md">
@@ -49,7 +95,40 @@
</span>
{{ _("Add to Cart") }}
</button>
- {% endif %}
+ {% endif %}
+
+ <!-- Add to Wishlist -->
+ {% if cart_settings.enable_wishlist %}
+ <a href="/wishlist"
+ class="btn btn-view-in-wishlist hidden"
+ role="button"
+ >
+ <span class="mr-2">
+ <svg class="icon icon-md">
+ <use href="#icon-heart"></use>
+ </svg>
+ </span>
+ {{ _("View in Wishlist") }}
+ </a>
+
+ {% set price = product_info.get("price") or {} %}
+ <button
+ data-item-code="{{item_code}}"
+ data-price="{{ price.get('price_list_rate') or 0}}"
+ data-formatted-price="{{ price.get('formatted_price') or 0 }}"
+ class="btn btn-add-to-wishlist"
+ >
+ <span class="mr-2">
+ <svg class="icon icon-md">
+ <use href="#icon-heart"></use>
+ </svg>
+ </span>
+ {{ _("Add to Wishlist") }}
+ </button>
+ {% endif %}
+ </div>
+
+ <!-- Contact Us -->
{% if cart_settings.show_contact_us_button %}
{% include "templates/generators/item/item_inquiry.html" %}
{% endif %}
@@ -60,6 +139,7 @@
<script>
frappe.ready(() => {
$('.page_content').on('click', '.btn-add-to-cart', (e) => {
+ // Bind action on add to cart button
const $btn = $(e.currentTarget);
$btn.prop('disabled', true);
const item_code = $btn.data('item-code');
@@ -74,7 +154,64 @@
}
});
});
+
+ $('.page_content').on('click', '.btn-add-to-wishlist', (e) => {
+ // Bind action on wishlist button
+ const $btn = $(e.currentTarget);
+ $btn.prop('disabled', true);
+
+ let args = {
+ item_code: $btn.data('item-code'),
+ price: $btn.data('price'),
+ formatted_price: $btn.data('formatted-price')
+ };
+ let failure_action = function() {
+ $btn.prop('disabled', false);
+ };
+ let success_action = function() {
+ $btn.prop('disabled', false);
+ erpnext.wishlist.set_wishlist_count();
+ $('.btn-add-to-wishlist, .btn-view-in-wishlist').toggleClass('hidden');
+
+ };
+ erpnext.wishlist.add_remove_from_wishlist("add", args, success_action, failure_action);
+ });
+
+ $('.page_content').on('click', '.offer-details', (e) => {
+ // Bind action on More link in Offers
+ const $btn = $(e.currentTarget);
+ $btn.prop('disabled', true);
+
+ var d = new frappe.ui.Dialog({
+ title: __($btn.data('offer-title')),
+ fields: [
+ {
+ fieldname: 'offer_details',
+ fieldtype: 'HTML'
+ },
+ {
+ fieldname: 'section_break',
+ fieldtype: 'Section Break'
+ }
+ ]
+ });
+
+ frappe.call({
+ method: 'erpnext.e_commerce.doctype.website_offer.website_offer.get_offer_details',
+ args: {
+ offer_id: $btn.data('offer-id')
+ },
+ callback: (value) => {
+ d.set_value("offer_details", value.message);
+ d.show();
+ $btn.prop('disabled', false);
+ }
+ })
+
+ });
});
+
+
</script>
{% endif %}
diff --git a/erpnext/templates/generators/item/item_configure.js b/erpnext/templates/generators/item/item_configure.js
index 8eadb84..5cb5d15 100644
--- a/erpnext/templates/generators/item/item_configure.js
+++ b/erpnext/templates/generators/item/item_configure.js
@@ -280,14 +280,14 @@
}
get_next_attribute_and_values(selected_attributes) {
- return this.call('erpnext.portal.product_configurator.utils.get_next_attribute_and_values', {
+ return this.call('erpnext.e_commerce.product_configurator.utils.get_next_attribute_and_values', {
item_code: this.item_code,
selected_attributes
});
}
get_attributes_and_values() {
- return this.call('erpnext.portal.product_configurator.utils.get_attributes_and_values', {
+ return this.call('erpnext.e_commerce.product_configurator.utils.get_attributes_and_values', {
item_code: this.item_code
});
}
diff --git a/erpnext/templates/generators/item/item_details.html b/erpnext/templates/generators/item/item_details.html
index 3b77585..cf6e2b9 100644
--- a/erpnext/templates/generators/item/item_details.html
+++ b/erpnext/templates/generators/item/item_details.html
@@ -1,11 +1,12 @@
-<div class="col-md-7 product-details">
+{% set width_class = "expand" if not slides else "" %}
+<div class="col-md-7 product-details {{ width_class }}">
<!-- title -->
<h1 class="product-title" itemprop="name">
- {{ item_name }}
+ {{ doc.web_item_name }}
</h1>
<p class="product-code">
<span>{{ _("Item Code") }}:</span>
- <span itemprop="productID">{{ doc.name }}</span>
+ <span itemprop="productID">{{ doc.item_code }}</span>
</p>
{% if has_variants %}
<!-- configure template -->
diff --git a/erpnext/templates/generators/item/item_image.html b/erpnext/templates/generators/item/item_image.html
index 39a30d0..4de3b62 100644
--- a/erpnext/templates/generators/item/item_image.html
+++ b/erpnext/templates/generators/item/item_image.html
@@ -1,4 +1,5 @@
-<div class="col-md-5 h-100 d-flex">
+{% set column_size = 5 if slides else 4 %}
+<div class="col-md-{{ column_size }} h-100 d-flex mb-4">
{% if slides %}
<div class="item-slideshow d-flex flex-column mr-3">
{% for item in slides %}
@@ -23,7 +24,7 @@
})
</script>
{% else %}
- {{ product_image(website_image or image or 'no-image.jpg', alt=website_image_alt or item_name) }}
+ {{ product_image(doc.website_image or doc.image or 'no-image.jpg', alt=doc.website_image_alt or doc.item_name) }}
{% endif %}
<!-- Simple image preview -->
diff --git a/erpnext/templates/generators/item/item_inquiry.js b/erpnext/templates/generators/item/item_inquiry.js
index 4724b68..0aee996 100644
--- a/erpnext/templates/generators/item/item_inquiry.js
+++ b/erpnext/templates/generators/item/item_inquiry.js
@@ -52,7 +52,7 @@
d.hide();
- frappe.call('erpnext.shopping_cart.cart.create_lead_for_item_inquiry', {
+ frappe.call('erpnext.e_commerce.shopping_cart.cart.create_lead_for_item_inquiry', {
lead: doc,
subject: values.subject,
message: values.message
diff --git a/erpnext/templates/generators/item/item_reviews.html b/erpnext/templates/generators/item/item_reviews.html
new file mode 100644
index 0000000..fd03a82
--- /dev/null
+++ b/erpnext/templates/generators/item/item_reviews.html
@@ -0,0 +1,82 @@
+{% from "erpnext/templates/includes/macros.html" import user_review, ratings_summary %}
+
+<div class="mt-12 ratings-reviews-section" style="display: flex;">
+ <div class="col-md-4 order-md-1 mt-8" style="max-width: 300px;">
+ {{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating) }}
+
+ <!-- Write a Review for legitimate users -->
+ {% if frappe.session.user != "Guest" %}
+ <button class="btn btn-light btn-write-review mr-2 mt-4 mb-4 w-100"
+ data-web-item="{{ doc.name }}">
+ {{ _("Write a Review") }}
+ </button>
+ {% endif %}
+ </div>
+
+ <!-- Reviews and Comments -->
+ <div class="col-12 order-2 col-md-9 order-md-2 mt-8 ml-16">
+ <h2 class="reviews-header">
+ {{ _("Reviews") }}
+ </h2>
+ {% if reviews %}
+ {{ user_review(reviews) }}
+
+ {% if total_reviews > 4 %}
+ <div class="mt-6 mb-6"style="color: var(--primary);">
+ <a href="/customer_reviews?item_code={{ doc.item_code }}">{{ _("View all reviews") }}</a>
+ </div>
+ {% endif %}
+
+ {% else %}
+ <h6 class="text-muted mt-6">
+ {{ _("No Reviews") }}
+ </h6>
+ {% endif %}
+ </div>
+</div>
+
+<script>
+ frappe.ready(() => {
+ $('.page_content').on('click', '.btn-write-review', (e) => {
+ // Bind action on write a review button
+ const $btn = $(e.currentTarget);
+
+ let d = new frappe.ui.Dialog({
+ title: __("Write a Review"),
+ fields: [
+ {fieldname: "title", fieldtype: "Data", label: "Headline", reqd: 1},
+ {fieldname: "rating", fieldtype: "Rating", label: "Overall Rating", reqd: 1},
+ {fieldtype: "Section Break"},
+ {fieldname: "comment", fieldtype: "Small Text", label: "Your Review"}
+ ],
+ primary_action: function() {
+ var data = d.get_values();
+ frappe.call({
+ method: "erpnext.e_commerce.doctype.item_review.item_review.add_item_review",
+ args: {
+ web_item: "{{ doc.name }}",
+ title: data.title,
+ rating: data.rating,
+ comment: data.comment
+ },
+ freeze: true,
+ freeze_message: __("Submitting Review ..."),
+ callback: function(r) {
+ if(!r.exc) {
+ frappe.msgprint({
+ message: __("Thank you for submitting your review"),
+ title: __("Review Submitted"),
+ indicator: "green"
+ });
+ d.hide();
+ location.reload();
+ }
+ }
+ });
+ },
+ primary_action_label: __('Submit')
+ });
+ d.show();
+ });
+ });
+</script>
diff --git a/erpnext/templates/generators/item/item_specifications.html b/erpnext/templates/generators/item/item_specifications.html
index d4dfa8e..f395761 100644
--- a/erpnext/templates/generators/item/item_specifications.html
+++ b/erpnext/templates/generators/item/item_specifications.html
@@ -1,14 +1,18 @@
-{% if doc.website_specifications -%}
-<div class="row item-website-specification mt-5">
- <div class="col-md-12">
- <table class="table table-bordered">
- {% for d in doc.website_specifications -%}
+<!-- Is reused to render within tabs as well as independently -->
+{% if website_specifications %}
+<div class="mt-5 item-website-specification">
+ <div class="col-md-11">
+ {% if not show_tabs %}
+ <h3 class="product-title mb-5 mt-8">Product Details</h3>
+ {% endif %}
+ <table class="table table-bordered table-hover">
+ {% for d in website_specifications -%}
<tr>
- <td class="text-muted" style="width: 30%;">{{ d.label }}</td>
+ <td class="text-muted" style="width: 30%; font-weight: bold;">{{ d.label }}</td>
<td>{{ d.description }}</td>
</tr>
{%- endfor %}
</table>
</div>
</div>
-{%- endif %}
+{% endif %}
diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html
index b5f18ba..a27b566 100644
--- a/erpnext/templates/generators/item_group.html
+++ b/erpnext/templates/generators/item_group.html
@@ -1,3 +1,4 @@
+{% from "erpnext/templates/includes/macros.html" import field_filter_section, attribute_filter_section, discount_range_filters %}
{% extends "templates/web.html" %}
{% block header %}
@@ -8,6 +9,12 @@
<script type="text/javascript" src="/all-products/index.js"></script>
{% endblock %}
+{% block breadcrumbs %}
+<div class="item-breadcrumbs small text-muted">
+ {% include "templates/includes/breadcrumbs.html" %}
+</div>
+{% endblock %}
+
{% block page_content %}
<div class="item-group-content" itemscope itemtype="http://schema.org/Product" data-item-group="{{ name }}">
<div class="item-group-slideshow">
@@ -27,6 +34,20 @@
</div>
<div class="row">
<div class="col-12 order-2 col-md-9 order-md-2 item-card-group-section">
+ {% if sub_categories %}
+ <div class="sub-category-container">
+ <div class="heading"> {{ _('Sub Categories') }} </div>
+ </div>
+ <div class="sub-category-container scroll-categories">
+ {% for row in sub_categories%}
+ <a href="{{ row.route or '#' }}" style="text-decoration: none;">
+ <div class="category-pill">
+ {{ row.name }}
+ </div>
+ </a>
+ {% endfor %}
+ </div>
+ {% endif %}
<div class="row products-list">
{% if items %}
{% for item in items %}
@@ -43,68 +64,16 @@
<div class="mb-4 filters-title" > {{ _('Filters') }} </div>
<a class="mb-4 clear-filters" href="/{{ doc.route }}">{{ _('Clear All') }}</a>
</div>
- {% for field_filter in field_filters %}
- {%- set item_field = field_filter[0] %}
- {%- set values = field_filter[1] %}
- <div class="mb-4 filter-block pb-5">
- <div class="filter-label mb-3">{{ item_field.label }}</div>
+ <!-- field filters -->
+ {{ field_filter_section(field_filters) }}
- {% if values | len > 20 %}
- <!-- show inline filter if values more than 20 -->
- <input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
- {% endif %}
+ <!-- attribute filters -->
+ {{ attribute_filter_section(attribute_filters) }}
- {% if values %}
- <div class="filter-options">
- {% for value in values %}
- <div class="checkbox" data-value="{{ value }}">
- <label for="{{value}}">
- <input type="checkbox"
- class="product-filter field-filter"
- id="{{value}}"
- data-filter-name="{{ item_field.fieldname }}"
- data-filter-value="{{ value }}"
- >
- <span class="label-area">{{ value }}</span>
- </label>
- </div>
- {% endfor %}
- </div>
- {% else %}
- <i class="text-muted">{{ _('No values') }}</i>
- {% endif %}
- </div>
- {% endfor %}
-
- {% for attribute in attribute_filters %}
- <div class="mb-4 filter-block pb-5">
- <div class="filter-label mb-3">{{ attribute.name}}</div>
- {% if values | len > 20 %}
- <!-- show inline filter if values more than 20 -->
- <input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
- {% endif %}
-
- {% if attribute.item_attribute_values %}
- <div class="filter-options">
- {% for attr_value in attribute.item_attribute_values %}
- <div class="checkbox">
- <label data-value="{{ value }}">
- <input type="checkbox"
- class="product-filter attribute-filter"
- id="{{attr_value.name}}"
- data-attribute-name="{{ attribute.name }}"
- data-attribute-value="{{ attr_value.attribute_value }}"
- {% if attr_value.checked %} checked {% endif %}>
- <span class="label-area">{{ attr_value.attribute_value }}</span>
- </label>
- </div>
- {% endfor %}
- </div>
- {% else %}
- <i class="text-muted">{{ _('No values') }}</i>
- {% endif %}
- </div>
- {% endfor %}
+ <!-- discount filters -->
+ {% if discount_filters %}
+ {{ discount_range_filters(discount_filters) }}
+ {% endif %}
</div>
<script>
diff --git a/erpnext/templates/includes/cart.js b/erpnext/templates/includes/cart.js
index c390cd1..4de8ff7 100644
--- a/erpnext/templates/includes/cart.js
+++ b/erpnext/templates/includes/cart.js
@@ -48,7 +48,7 @@
const address_name = $card.closest('[data-address-name]').attr('data-address-name');
frappe.call({
type: "POST",
- method: "erpnext.shopping_cart.cart.update_cart_address",
+ method: "erpnext.e_commerce.shopping_cart.cart.update_cart_address",
freeze: true,
args: {
address_type,
@@ -185,7 +185,7 @@
return frappe.call({
btn: btn,
type: "POST",
- method: "erpnext.shopping_cart.cart.apply_shipping_rule",
+ method: "erpnext.e_commerce.shopping_cart.cart.apply_shipping_rule",
args: { shipping_rule: rule },
callback: function(r) {
if(!r.exc) {
@@ -198,7 +198,7 @@
place_order: function(btn) {
return frappe.call({
type: "POST",
- method: "erpnext.shopping_cart.cart.place_order",
+ method: "erpnext.e_commerce.shopping_cart.cart.place_order",
btn: btn,
callback: function(r) {
if(r.exc) {
@@ -223,7 +223,7 @@
request_quotation: function(btn) {
return frappe.call({
type: "POST",
- method: "erpnext.shopping_cart.cart.request_for_quotation",
+ method: "erpnext.e_commerce.shopping_cart.cart.request_for_quotation",
btn: btn,
callback: function(r) {
if(r.exc) {
@@ -254,7 +254,7 @@
apply_coupon_code: function(btn) {
return frappe.call({
type: "POST",
- method: "erpnext.shopping_cart.cart.apply_coupon_code",
+ method: "erpnext.e_commerce.shopping_cart.cart.apply_coupon_code",
btn: btn,
args : {
applied_code : $('.txtcoupon').val(),
diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html
index 4482bc1..a9377f3 100644
--- a/erpnext/templates/includes/cart/cart_address.html
+++ b/erpnext/templates/includes/cart/cart_address.html
@@ -4,7 +4,7 @@
{% set select_address = True %}
{% endif %}
-{% set show_coupon_code = frappe.db.get_single_value('Shopping Cart Settings', 'show_apply_coupon_code_in_website') %}
+{% set show_coupon_code = frappe.db.get_single_value('E Commerce Settings', 'show_apply_coupon_code_in_website') %}
{% if show_coupon_code == 1%}
<div class="mb-3">
<div class="row no-gutters">
@@ -130,10 +130,10 @@
],
primary_action_label: __('Save'),
primary_action: (values) => {
- frappe.call('erpnext.shopping_cart.cart.add_new_address', { doc: values })
+ frappe.call('erpnext.e_commerce.shopping_cart.cart.add_new_address', { doc: values })
.then(r => {
frappe.call({
- method: "erpnext.shopping_cart.cart.update_cart_address",
+ method: "erpnext.e_commerce.shopping_cart.cart.update_cart_address",
args: {
address_type: r.message.address_type,
address_name: r.message.name
diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html
index be0d47f..d371352 100644
--- a/erpnext/templates/includes/macros.html
+++ b/erpnext/templates/includes/macros.html
@@ -59,13 +59,17 @@
{% endmacro %}
-{%- macro item_card(title, image, url, description, rate, category, is_featured=False, is_full_width=False, align="Left") -%}
+{%- macro item_card(item, settings=None, is_featured=False, is_full_width=False, align="Left") -%}
{%- set align_items_class = resolve_class({
'align-items-end': align == 'Right',
'align-items-center': align == 'Center',
'align-items-start': align == 'Left',
}) -%}
{%- set col_size = 3 if is_full_width else 4 -%}
+{%- set title = item.item_name or item.item_code -%}
+{%- set image = item.website_image or item.image -%}
+{%- set description = item.website_description or item.description-%}
+
{% if is_featured %}
<div class="col-sm-{{ col_size*2 }} item-card">
<div class="card featured-item {{ align_items_class }}">
@@ -75,12 +79,12 @@
<img class="card-img" src="{{ image }}" alt="{{ title }}">
</div>
<div class="col-md-6">
- {{ item_card_body(title, description, url, rate, category, is_featured, align) }}
+ {{ item_card_body(title, settings, description, item, is_featured, align) }}
</div>
</div>
{% else %}
<div class="col-md-12">
- {{ item_card_body(title, description, url, rate, category, is_featured, align) }}
+ {{ item_card_body(title, settings, description, item, is_featured, align) }}
</div>
{% endif %}
</div>
@@ -89,35 +93,312 @@
<div class="col-sm-{{ col_size }} item-card">
<div class="card {{ align_items_class }}">
{% if image %}
- <div class="card-img-container">
- <img class="card-img" src="{{ image }}" alt="{{ title }}">
- </div>
+ <div class="card-img-container">
+ <a href="/{{ item.route or '#' }}" style="text-decoration: none;">
+ <img class="card-img" src="{{ image }}" alt="{{ title }}">
+ </a>
+ </div>
{% else %}
- <div class="card-img-top no-image">
- {{ frappe.utils.get_abbr(title) }}
- </div>
+ <a href="/{{ item.route or '#' }}" style="text-decoration: none;">
+ <div class="card-img-top no-image">
+ {{ frappe.utils.get_abbr(title) }}
+ </div>
+ </a>
{% endif %}
- {{ item_card_body(title, description, url, rate, category, is_featured, align) }}
+ {{ item_card_body(title, settings, description, item, is_featured, align) }}
</div>
</div>
{% endif %}
{%- endmacro -%}
-{%- macro item_card_body(title, description, url, rate, category, is_featured, align) -%}
+{%- macro item_card_body(title, settings, description, item, is_featured, align) -%}
{%- set align_class = resolve_class({
'text-right': align == 'Right',
'text-center': align == 'Center' and not is_featured,
'text-left': align == 'Left' or is_featured,
}) -%}
-<div class="card-body {{ align_class }}">
- <div class="product-title">{{ title or '' }}</div>
+<div class="card-body {{ align_class }}" style="width:100%">
+ <div style="margin-top: 16px; display: flex;">
+ <a href="/{{ item.route or '#' }}">
+ <div class="product-title">{{ title or '' }}</div>
+ </a>
+ {% if item.in_stock %}
+ <span class="indicator {{ item.in_stock }} card-indicator"></span>
+ {% endif %}
+ {% if not item.has_variants and settings.enable_wishlist %}
+ <div class="like-action"
+ data-item-code="{{ item.item_code }}"
+ data-price="{{ item.price }}"
+ data-formatted-price="{{ item.get('formatted_price') }}">
+ <svg class="icon sm">
+ {%- set icon_class = "wished" if item.wished else "not-wished"-%}
+ <use class="{{ icon_class }} wish-icon" href="#icon-heart"></use>
+ </svg>
+ </div>
+ {% endif %}
+ </div>
{% if is_featured %}
- <div class="product-price">{{ rate or '' }}</div>
- <div class="product-description ellipsis">{{ description or '' }}</div>
+ <div class="product-price">{{ item.formatted_price or '' }}</div>
+ <div class="product-description ellipsis">{{ description or '' }}</div>
{% else %}
- <div class="product-category">{{ category or '' }}</div>
- <div class="product-price">{{ rate or '' }}</div>
+ <div class="product-category">{{ item.item_group or '' }}</div>
+
+ {% if item.formatted_price %}
+ <div class="product-price">
+ {{ item.formatted_price or '' }}
+
+ {% if item.get("formatted_mrp") %}
+ <small class="ml-1 text-muted">
+ <s>{{ item.formatted_mrp }}</s>
+ </small>
+ <small class="ml-1" style="color: #F47A7A; font-weight: 500;">
+ {{ item.discount }} OFF
+ </small>
+ {% endif %}
+
+ </div>
+ {% endif %}
+
+ {% if item.has_variants %}
+ <a href="/{{ item.route or '#' }}">
+ <div class="btn btn-sm btn-explore-variants w-100 mt-4">
+ {{ _('Explore') }}
+ </div>
+ </a>
+ {% elif settings.enabled and (settings.allow_items_not_in_stock or item.in_stock != "red")%}
+ <div id="{{ item.name }}" class="btn btn-sm btn-add-to-cart-list not-added w-100 mt-4"
+ data-item-code="{{ item.item_code }}">
+ {{ _('Add to Cart') }}
+ </div>
+ {% endif %}
{% endif %}
</div>
-<a href="/{{ url or '#' }}" class="stretched-link"></a>
+{%- endmacro -%}
+
+
+{%- macro wishlist_card(item, settings) %}
+<div class="col-sm-3 wishlist-card">
+ <div class="card text-center" style="max-height: 390px;">
+ {% if item.image %}
+ <div class="card-img-container">
+ <a href="/{{ item.route or '#' }}" style="text-decoration: none;">
+ <img class="card-img" src="{{ item.image }}" alt="{{ title }}">
+ </a>
+ <div class="remove-wish" data-item-code="{{ item.item_code }}">
+ <span style="padding-bottom: 2px;">
+ <svg class="icon sm remove-wish-icon" style="margin-bottom: 4px; margin-left: 0.5px;">
+ <use class="close" href="#icon-close"></use>
+ </svg>
+ </span>
+ </div>
+
+ </div>
+ {% else %}
+ <a href="/{{ item.route or '#' }}" style="text-decoration: none;">
+ <div class="card-img-top no-image">
+ {{ frappe.utils.get_abbr(title) }}
+ </div>
+ </a>
+ {% endif %}
+
+ {{ wishlist_card_body(item, settings) }}
+
+
+ </div>
+</div>
+{%- endmacro -%}
+
+{%- macro wishlist_card_body(item, settings) %}
+<div class="card-body text-center" style="width: 100%;">
+ <div style="margin-top: 16px;">
+ <div class="product-title">{{ item.item_name or item.item_code or ''}}</div>
+ </div>
+ <div class="product-price">
+ {{ item.formatted_price or '' }}
+
+ {% if item.get("formatted_mrp") %}
+ <small class="ml-1 text-muted">
+ <s>{{ item.formatted_mrp }}</s>
+ </small>
+ <small class="ml-1" style="color: #F47A7A; font-weight: 500;">
+ {{ item.discount }} OFF
+ </small>
+ {% endif %}
+ </div>
+
+ {% if (item.available and settings.show_stock_availability) or (not settings.show_stock_availability) %}
+ <!-- Show move to cart button if in stock or if showing stock availability is disabled -->
+ <button data-item-code="{{ item.item_code}}" class="btn btn-add-to-cart w-100 wishlist-cart-not-added mt-2">
+ <span class="mr-2">
+ <svg class="icon icon-md">
+ <use href="#icon-assets"></use>
+ </svg>
+ </span>
+ {{ _("Move to Cart") }}
+ </button>
+ {% else %}
+ <div class="mt-4" style="color: #F47A7A; width: 100%;">
+ {{ _("Not in Stock") }}
+ </div>
+ {% endif %}
+</div>
+{%- endmacro -%}
+
+{%- macro ratings_with_title(avg_rating, title, size, rating_header_class) -%}
+<div style="display: flex;">
+ <div class="rating">
+ {% for i in range(1,6) %}
+ {% set fill_class = 'star-click' if i <= avg_rating else '' %}
+ <svg class="icon icon-{{ size }} {{ fill_class }}">
+ <use href="#icon-star"></use>
+ </svg>
+ {% endfor %}
+ </div>
+ <p class="ml-4 {{ rating_header_class }}">
+ <span>{{ title }}</span>
+ </p>
+</div>
+{%- endmacro -%}
+
+{%- macro ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating)-%}
+<!-- Ratings Summary -->
+<h2 class="reviews-header">
+ {{ _("Customer Ratings") }}
+</h2>
+
+{% if reviews %}
+ {% set rating_title = frappe.utils.cstr(average_rating) + " " + _("out of 5") %}
+ {{ ratings_with_title(average_whole_rating, rating_title, "lg", "rating-summary-title") }}
+{% endif %}
+
+<!-- Rating Progress Bars -->
+<div class="rating-progress-bar-section">
+ {% for percent in reviews_per_rating %}
+ <div class="mt-4 col-sm-4 small rating-bar-title">
+ {{ loop.index }} star
+ </div>
+ <div class="row">
+ <div class="col-md-7">
+ <div class="progress rating-progress-bar" title="{{ percent }} % of reviews are {{ loop.index }} star">
+ <div class="progress-bar" role="progressbar"
+ aria-valuenow="{{ percent }}"
+ aria-valuemin="0" aria-valuemax="100"
+ style="width: {{ percent }}%; background-color: var(--text-on-green);">
+ </div>
+ </div>
+ </div>
+ <div class="col-sm-1 small">
+ {{ percent }}%
+ </div>
+ </div>
+ {% endfor %}
+</div>
+{%- endmacro -%}
+
+{%- macro user_review(reviews)-%}
+<!-- User Reviews -->
+<div class="user-reviews">
+ {% for review in reviews %}
+ <div class="mb-3 review">
+ {{ ratings_with_title(review.rating, _(review.review_title), "md", "user-review-title") }}
+
+ <div class="review-signature">
+ <span class="reviewer">{{ _(review.customer) }}</span>
+ <span>{{ review.published_on }}</span>
+ </div>
+ <div class="product-description mb-4 mt-4">
+ <p>
+ {{ _(review.comment) }}
+ </p>
+ </div>
+ </div>
+ {% endfor %}
+</div>
+{%- endmacro -%}
+
+{%- macro field_filter_section(filters)-%}
+{% for field_filter in filters %}
+ {%- set item_field = field_filter[0] %}
+ {%- set values = field_filter[1] %}
+ <div class="mb-4 filter-block pb-5">
+ <div class="filter-label mb-3">{{ item_field.label }}</div>
+
+ {% if values | len > 20 %}
+ <!-- show inline filter if values more than 20 -->
+ <input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
+ {% endif %}
+
+ {% if values %}
+ <div class="filter-options">
+ {% for value in values %}
+ <div class="checkbox" data-value="{{ value }}">
+ <label for="{{value}}">
+ <input type="checkbox"
+ class="product-filter field-filter"
+ id="{{value}}"
+ data-filter-name="{{ item_field.fieldname }}"
+ data-filter-value="{{ value }}">
+ <span class="label-area">{{ value }}</span>
+ </label>
+ </div>
+ {% endfor %}
+ </div>
+ {% else %}
+ <i class="text-muted">{{ _('No values') }}</i>
+ {% endif %}
+ </div>
+{% endfor %}
+{%- endmacro -%}
+
+{%- macro attribute_filter_section(filters)-%}
+{% for attribute in filters %}
+ <div class="mb-4 filter-block pb-5">
+ <div class="filter-label mb-3">{{ attribute.name}}</div>
+ {% if values | len > 20 %}
+ <!-- show inline filter if values more than 20 -->
+ <input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
+ {% endif %}
+
+ {% if attribute.item_attribute_values %}
+ <div class="filter-options">
+ {% for attr_value in attribute.item_attribute_values %}
+ <div class="checkbox">
+ <label data-value="{{ value }}">
+ <input type="checkbox"
+ class="product-filter attribute-filter"
+ id="{{attr_value.name}}"
+ data-attribute-name="{{ attribute.name }}"
+ data-attribute-value="{{ attr_value.attribute_value }}"
+ {% if attr_value.checked %} checked {% endif %}>
+ <span class="label-area">{{ attr_value.attribute_value }}</span>
+ </label>
+ </div>
+ {% endfor %}
+ </div>
+ {% else %}
+ <i class="text-muted">{{ _('No values') }}</i>
+ {% endif %}
+ </div>
+{% endfor %}
+{%- endmacro -%}
+
+{%- macro discount_range_filters(filters)-%}
+<div class="mb-4 filter-block pb-5">
+ <div class="filter-label mb-3">{{ _("Discounts") }}</div>
+ <div class="filter-options">
+ {% for entry in filters %}
+ <div class="checkbox">
+ <label data-value="{{ entry[0] }}">
+ <input type="radio" class="product-filter discount-filter"
+ name="discount" id="{{ entry[0] }}"
+ data-filter-name="discount" data-filter-value="{{ entry[0] }}"
+ >
+ <span class="label-area" for="{{ entry[0] }}">
+ {{ entry[1] }}
+ </span>
+ </label>
+ </div>
+ {% endfor %}
+ </div>
+</div>
{%- endmacro -%}
diff --git a/erpnext/templates/includes/navbar/navbar_items.html b/erpnext/templates/includes/navbar/navbar_items.html
index 2912206..3275521 100644
--- a/erpnext/templates/includes/navbar/navbar_items.html
+++ b/erpnext/templates/includes/navbar/navbar_items.html
@@ -6,7 +6,17 @@
<svg class="icon icon-lg">
<use href="#icon-assets"></use>
</svg>
- <span class="badge badge-primary cart-badge" id="cart-count"></span>
+ <span class="badge badge-primary shopping-badge" id="cart-count"></span>
</a>
- </li>
+ </li>
+ {% if frappe.db.get_single_value("E Commerce Settings", "enable_wishlist") %}
+ <li class="wishlist wishlist-icon hidden">
+ <a class="nav-link" href="/wishlist">
+ <svg class="icon icon-lg">
+ <use href="#icon-heart-active"></use>
+ </svg>
+ <span class="badge badge-primary shopping-badge" id="wish-count"></span>
+ </a>
+ </li>
+ {% endif %}
{% endblock %}
diff --git a/erpnext/templates/includes/product_page.js b/erpnext/templates/includes/product_page.js
index 90a1d86..a3979d0 100644
--- a/erpnext/templates/includes/product_page.js
+++ b/erpnext/templates/includes/product_page.js
@@ -7,7 +7,7 @@
frappe.call({
type: "POST",
- method: "erpnext.shopping_cart.product_info.get_product_info_for_website",
+ method: "erpnext.e_commerce.shopping_cart.product_info.get_product_info_for_website",
args: {
item_code: get_item_code()
},
diff --git a/erpnext/templates/includes/products_as_list.html b/erpnext/templates/includes/products_as_list.html
index 9bf9fd9..976d614 100644
--- a/erpnext/templates/includes/products_as_list.html
+++ b/erpnext/templates/includes/products_as_list.html
@@ -1,4 +1,4 @@
-{% from "erpnext/templates/includes/macros.html" import item_card, item_card_body %}
+{% from "erpnext/templates/includes/macros.html" import item_card, item_card_body, product_image_square %}
<a class="product-link product-list-link" href="{{ route|abs_url }}">
<div class='row'>
diff --git a/erpnext/templates/pages/cart.html b/erpnext/templates/pages/cart.html
index c64c634..0c7993b 100644
--- a/erpnext/templates/pages/cart.html
+++ b/erpnext/templates/pages/cart.html
@@ -96,7 +96,7 @@
})
});
function show_terms_and_conditions(terms_name) {
- frappe.call('erpnext.shopping_cart.cart.get_terms_and_conditions', { terms_name })
+ frappe.call('erpnext.e_commerce.shopping_cart.cart.get_terms_and_conditions', { terms_name })
.then(r => {
frappe.msgprint({
title: terms_name,
diff --git a/erpnext/templates/pages/cart.py b/erpnext/templates/pages/cart.py
index 7c441f7..1f0cd5c 100644
--- a/erpnext/templates/pages/cart.py
+++ b/erpnext/templates/pages/cart.py
@@ -1,11 +1,9 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
no_cache = 1
-
-from erpnext.shopping_cart.cart import get_cart_quotation
+from erpnext.e_commerce.shopping_cart.cart import get_cart_quotation
def get_context(context):
diff --git a/erpnext/templates/pages/customer_reviews.html b/erpnext/templates/pages/customer_reviews.html
new file mode 100644
index 0000000..e11da3d
--- /dev/null
+++ b/erpnext/templates/pages/customer_reviews.html
@@ -0,0 +1,54 @@
+{% extends "templates/web.html" %}
+{% from "erpnext/templates/includes/macros.html" import user_review, ratings_summary %}
+
+{% block title %} {{ _("Customer Reviews") }} {% endblock %}
+
+{% block page_content %}
+<div class="product-container col-md-12">
+<div style="display: flex;">
+ <div class="col-md-4 order-md-1 mt-8" style="max-width: 300px;">
+ {{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating) }}
+
+ <!-- Write a Review for legitimate users -->
+ {% if frappe.session.user != "Guest" %}
+ <button class="btn btn-light btn-write-review mr-2 mt-4 mb-4 w-100"
+ data-web-item="{{ web_item }}">
+ {{ _("Write a Review") }}
+ </button>
+ {% endif %}
+ </div>
+
+ <!-- Reviews and Comments -->
+ <div class="col-12 order-2 col-md-9 order-md-2 mt-8 ml-16">
+ <h2 class="reviews-header">
+ {{ _("Reviews") }}
+ </h2>
+ {% if reviews %}
+ {{ user_review(reviews) }}
+
+ {% if not reviews | len >= total_reviews %}
+ <button class="btn btn-light btn-view-more mr-2 mt-4 mb-4 w-30"
+ data-web-item="{{ web_item }}">
+ {{ _("View More") }}
+ </button>
+ {% endif %}
+
+ {% else %}
+ <h6 class="text-muted mt-6">
+ {{ _("No Reviews") }}
+ </h6>
+ {% endif %}
+ </div>
+</div>
+</div>
+
+{% endblock %}
+
+{% block base_scripts %}
+<!-- js should be loaded in body! -->
+<script type="text/javascript" src="/assets/frappe/js/lib/jquery/jquery.min.js"></script>
+<script type="text/javascript" src="/assets/js/frappe-web.min.js"></script>
+<script type="text/javascript" src="/assets/js/control.min.js"></script>
+<script type="text/javascript" src="/assets/js/dialog.min.js"></script>
+<script type="text/javascript" src="/assets/js/bootstrap-4-web.min.js"></script>
+{% endblock %}
\ No newline at end of file
diff --git a/erpnext/templates/pages/customer_reviews.js b/erpnext/templates/pages/customer_reviews.js
new file mode 100644
index 0000000..9be12c7
--- /dev/null
+++ b/erpnext/templates/pages/customer_reviews.js
@@ -0,0 +1,134 @@
+$(() => {
+ class CustomerReviews {
+ constructor() {
+ this.bind_button_actions();
+ this.start = 0;
+ this.page_length = 10;
+ }
+
+ bind_button_actions() {
+ this.write_review();
+ this.view_more();
+ }
+
+ write_review() {
+ //TODO: make dialog popup on stray page
+ $('.page_content').on('click', '.btn-write-review', (e) => {
+ // Bind action on write a review button
+ const $btn = $(e.currentTarget);
+
+ let d = new frappe.ui.Dialog({
+ title: __("Write a Review"),
+ fields: [
+ {fieldname: "title", fieldtype: "Data", label: "Headline", reqd: 1},
+ {fieldname: "rating", fieldtype: "Rating", label: "Overall Rating", reqd: 1},
+ {fieldtype: "Section Break"},
+ {fieldname: "comment", fieldtype: "Small Text", label: "Your Review"}
+ ],
+ primary_action: function() {
+ let data = d.get_values();
+ frappe.call({
+ method: "erpnext.e_commerce.doctype.item_review.item_review.add_item_review",
+ args: {
+ web_item: $btn.attr('data-web-item'),
+ title: data.title,
+ rating: data.rating,
+ comment: data.comment
+ },
+ freeze: true,
+ freeze_message: __("Submitting Review ..."),
+ callback: (r) => {
+ if (!r.exc) {
+ frappe.msgprint({
+ message: __("Thank you for submitting your review"),
+ title: __("Review Submitted"),
+ indicator: "green"
+ });
+ d.hide();
+ location.reload();
+ }
+ }
+ });
+ },
+ primary_action_label: __('Submit')
+ });
+ d.show();
+ });
+ }
+
+ view_more() {
+ $('.page_content').on('click', '.btn-view-more', (e) => {
+ // Bind action on view more button
+ const $btn = $(e.currentTarget);
+ $btn.prop('disabled', true);
+
+ this.start += this.page_length;
+ let me = this;
+
+ frappe.call({
+ method: "erpnext.e_commerce.doctype.item_review.item_review.get_item_reviews",
+ args: {
+ web_item: $btn.attr('data-web-item'),
+ start: me.start,
+ end: me.page_length
+ },
+ callback: (result) => {
+ if (result.message) {
+ let res = result.message;
+ me.get_user_review_html(res.reviews);
+
+ $btn.prop('disabled', false);
+ if (res.total_reviews <= (me.start + me.page_length)) {
+ $btn.hide();
+ }
+
+ }
+ }
+ });
+ });
+
+ }
+
+ get_user_review_html(reviews) {
+ let me = this;
+ let $content = $('.user-reviews');
+
+ reviews.forEach((review) => {
+ $content.append(`
+ <div class="mb-3 review">
+ <div style="display: flex;">
+ <div class="rating">
+ ${me.get_review_stars(review.rating)}
+ </div>
+ <p class="ml-4 user-review-title">
+ <span>${__(review.review_title)}</span>
+ </p>
+ </div>
+ <div class="review-signature">
+ <span class="reviewer">${__(review.customer)}</span>
+ <span>${__(review.published_on)}</span>
+ </div>
+ <div class="product-description mb-4 mt-4">
+ <p>
+ ${__(review.comment)}
+ </p>
+ </div>
+ </div>
+ `);
+ });
+ }
+
+ get_review_stars(rating) {
+ let stars = ``;
+ for (let i = 1; i < 6; i++) {
+ let fill_class = i <= rating ? 'star-click' : '';
+ stars += `<svg class="icon icon-md ${fill_class}">
+ <use href="#icon-star"></use>
+ </svg>`;
+ }
+ return stars;
+ }
+ }
+
+ new CustomerReviews();
+});
\ No newline at end of file
diff --git a/erpnext/templates/pages/customer_reviews.py b/erpnext/templates/pages/customer_reviews.py
new file mode 100644
index 0000000..3bb0142
--- /dev/null
+++ b/erpnext/templates/pages/customer_reviews.py
@@ -0,0 +1,16 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+from __future__ import unicode_literals
+
+no_cache = 1
+
+import frappe
+from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
+
+def get_context(context):
+ context.full_page = True
+ context.reviews = None
+ if frappe.form_dict and frappe.form_dict.get("item_code"):
+ context.item_code = frappe.form_dict.get("item_code")
+ context.web_item = frappe.db.get_value("Website Item", {"item_code": context.item_code}, "name")
+ get_item_reviews(context.web_item, 0, 10, context)
diff --git a/erpnext/templates/pages/home.py b/erpnext/templates/pages/home.py
index 97a66fc..3e23fc7 100644
--- a/erpnext/templates/pages/home.py
+++ b/erpnext/templates/pages/home.py
@@ -11,7 +11,7 @@
homepage = frappe.get_doc('Homepage')
for item in homepage.products:
- route = frappe.db.get_value('Item', item.item_code, 'route')
+ route = frappe.db.get_value('Website Item', {"item_code": item.item_code}, 'route')
if route:
item.route = '/' + route
diff --git a/erpnext/templates/pages/order.py b/erpnext/templates/pages/order.py
index d4e81ab..59df433 100644
--- a/erpnext/templates/pages/order.py
+++ b/erpnext/templates/pages/order.py
@@ -6,10 +6,7 @@
import frappe
from frappe import _
-from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
- show_attachments,
-)
-
+from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import show_attachments
def get_context(context):
context.no_cache = 1
@@ -26,7 +23,7 @@
context.payment_ref = frappe.db.get_value("Payment Request",
{"reference_name": frappe.form_dict.name}, "name")
- context.enabled_checkout = frappe.get_doc("Shopping Cart Settings").enable_checkout
+ context.enabled_checkout = frappe.get_doc("E Commerce Settings").enable_checkout
default_print_format = frappe.db.get_value('Property Setter', dict(property='default_print_format', doc_type=frappe.form_dict.doctype), "value")
if default_print_format:
diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py
index 1b9df2b..d041cc0 100644
--- a/erpnext/templates/pages/product_search.py
+++ b/erpnext/templates/pages/product_search.py
@@ -1,16 +1,25 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
-
import frappe
from frappe.utils import cint, cstr, nowdate
from erpnext.setup.doctype.item_group.item_group import get_item_for_list_in_html
-from erpnext.shopping_cart.product_info import set_product_info_for_website
+from erpnext.e_commerce.shopping_cart.product_info import set_product_info_for_website
+
+# For SEARCH -------
+from redisearch import AutoCompleter, Client, Query
+from erpnext.e_commerce.website_item_indexing import (
+ WEBSITE_ITEM_INDEX,
+ WEBSITE_ITEM_NAME_AUTOCOMPLETE,
+ WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE,
+ make_key
+)
+# -----------------
no_cache = 1
+
def get_context(context):
context.show_search = True
@@ -49,3 +58,56 @@
set_product_info_for_website(item)
return [get_item_for_list_in_html(r) for r in data]
+
+@frappe.whitelist(allow_guest=True)
+def search(query, limit=10, fuzzy_search=True):
+ if not query:
+ # TODO: return top searches
+ return []
+
+ red = frappe.cache()
+
+ query = clean_up_query(query)
+
+ ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=red)
+ client = Client(make_key(WEBSITE_ITEM_INDEX), conn=red)
+ suggestions = ac.get_suggestions(
+ query,
+ num=limit,
+ fuzzy= fuzzy_search and len(query) > 4 # Fuzzy on length < 3 can be real slow
+ )
+
+ # Build a query
+ query_string = query
+
+ for s in suggestions:
+ query_string += f"|('{clean_up_query(s.string)}')"
+
+ q = Query(query_string)
+
+ print(f"Executing query: {q.query_string()}")
+
+ results = client.search(q)
+ results = list(map(convert_to_dict, results.docs))
+
+ # FOR DEBUGGING
+ print("SEARCH RESULTS ------------------\n ", results)
+
+ return results
+
+def clean_up_query(query):
+ return ''.join(c for c in query if c.isalnum() or c.isspace())
+
+def convert_to_dict(redis_search_doc):
+ return redis_search_doc.__dict__
+
+@frappe.whitelist(allow_guest=True)
+def get_category_suggestions(query):
+ if not query:
+ # TODO: return top searches
+ return []
+
+ ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=frappe.cache())
+ suggestions = ac.get_suggestions(query, num=10)
+
+ return [s.string for s in suggestions]
\ No newline at end of file
diff --git a/erpnext/templates/pages/wishlist.html b/erpnext/templates/pages/wishlist.html
new file mode 100644
index 0000000..4c039e3
--- /dev/null
+++ b/erpnext/templates/pages/wishlist.html
@@ -0,0 +1,24 @@
+{% extends "templates/web.html" %}
+
+{% block title %} {{ _("Wishlist") }} {% endblock %}
+
+{% block header %}<h3 class="shopping-cart-header mt-2 mb-6">{{ _("Wishlist") }}</h1>{% endblock %}
+
+{% block page_content %}
+{% if items %}
+ <div class="row">
+ <div class="col-md-12 item-card-group-section">
+ <div class="row products-list">
+ {% from "erpnext/templates/includes/macros.html" import wishlist_card %}
+ {% for item in items %}
+ {{ wishlist_card(item, settings) }}
+ {% endfor %}
+ </div>
+ </div>
+ </div>
+{% else %}
+ <!-- TODO: Make empty state for wishlist -->
+ {% include "erpnext/www/all-products/not_found.html" %}
+{% endif %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/erpnext/templates/pages/wishlist.py b/erpnext/templates/pages/wishlist.py
new file mode 100644
index 0000000..e534a23
--- /dev/null
+++ b/erpnext/templates/pages/wishlist.py
@@ -0,0 +1,57 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+from __future__ import unicode_literals
+
+no_cache = 1
+
+import frappe
+from erpnext.utilities.product import get_price
+from erpnext.e_commerce.shopping_cart.cart import _set_price_list
+
+def get_context(context):
+ settings = frappe.get_doc("E Commerce Settings")
+ items = get_wishlist_items()
+ selling_price_list = _set_price_list(settings)
+
+ for item in items:
+ if settings.show_stock_availability:
+ item.available = get_stock_availability(item.item_code, item.get("warehouse"))
+
+ price_details = get_price(
+ item.item_code,
+ selling_price_list,
+ settings.default_customer_group,
+ settings.company
+ )
+
+ if price_details:
+ item.formatted_mrp = price_details.get('formatted_mrp')
+ if item.formatted_mrp:
+ item.discount = price_details.get('formatted_discount_percent') or \
+ price_details.get('formatted_discount_rate')
+
+ context.items = items
+ context.settings = settings
+
+def get_stock_availability(item_code, warehouse):
+ stock_qty = frappe.utils.flt(
+ frappe.db.get_value("Bin",
+ {
+ "item_code": item_code,
+ "warehouse": warehouse
+ },
+ "actual_qty")
+ )
+ return True if stock_qty else False
+
+def get_wishlist_items():
+ if frappe.db.exists("Wishlist", frappe.session.user):
+ return frappe.db.sql("""
+ Select
+ item_code, item_name, website_item, price,
+ warehouse, image, item_group, route, formatted_price
+ from
+ `tabWishlist Items`
+ where
+ parent=%(user)s""" % {"user": frappe.db.escape(frappe.session.user)}, as_dict=1)
+ return
\ No newline at end of file
diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py
index e567f77..cbb430f 100644
--- a/erpnext/utilities/product.py
+++ b/erpnext/utilities/product.py
@@ -1,24 +1,21 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
-
import frappe
from frappe.utils import cint, flt, fmt_money, getdate, nowdate
from erpnext.accounts.doctype.pricing_rule.pricing_rule import get_pricing_rule_for_item
from erpnext.stock.doctype.batch.batch import get_batch_qty
-
-def get_qty_in_stock(item_code, item_warehouse_field, warehouse=None):
+def get_web_item_qty_in_stock(item_code, item_warehouse_field, warehouse=None):
in_stock, stock_qty = 0, ''
template_item_code, is_stock_item = frappe.db.get_value("Item", item_code, ["variant_of", "is_stock_item"])
if not warehouse:
- warehouse = frappe.db.get_value("Item", item_code, item_warehouse_field)
+ warehouse = frappe.db.get_value("Website Item", {"item_code": item_code}, item_warehouse_field)
if not warehouse and template_item_code and template_item_code != item_code:
- warehouse = frappe.db.get_value("Item", template_item_code, item_warehouse_field)
+ warehouse = frappe.db.get_value("Website Item", {"item_code": template_item_code}, item_warehouse_field)
if warehouse:
stock_qty = frappe.db.sql("""
@@ -93,17 +90,27 @@
"for_shopping_cart": True,
"currency": frappe.db.get_value("Price List", price_list, "currency")
}))
+ price_obj = price[0]
if pricing_rule:
+ # price without any rules applied
+ mrp = price_obj.price_list_rate or 0
+
if pricing_rule.pricing_rule_for == "Discount Percentage":
- price[0].price_list_rate = flt(price[0].price_list_rate * (1.0 - (flt(pricing_rule.discount_percentage) / 100.0)))
+ price_obj.discount_percent = pricing_rule.discount_percentage
+ price_obj.formatted_discount_percent = str(flt(pricing_rule.discount_percentage, 0)) + "%"
+ price_obj.price_list_rate = flt(price_obj.price_list_rate * (1.0 - (flt(pricing_rule.discount_percentage) / 100.0)))
if pricing_rule.pricing_rule_for == "Rate":
- price[0].price_list_rate = pricing_rule.price_list_rate
+ rate_discount = flt(mrp) - flt(pricing_rule.price_list_rate)
+ if rate_discount > 0:
+ price_obj.formatted_discount_rate = fmt_money(rate_discount, currency=price_obj["currency"])
+ price_obj.price_list_rate = pricing_rule.price_list_rate or 0
- price_obj = price[0]
if price_obj:
price_obj["formatted_price"] = fmt_money(price_obj["price_list_rate"], currency=price_obj["currency"])
+ if mrp != price_obj["price_list_rate"]:
+ price_obj["formatted_mrp"] = fmt_money(mrp, currency=price_obj["currency"])
price_obj["currency_symbol"] = not cint(frappe.db.get_default("hide_currency_symbol")) \
and (frappe.db.get_value("Currency", price_obj.currency, "symbol", cache=True) or price_obj.currency) \
@@ -124,15 +131,15 @@
price_obj["currency"] = ""
if not price_obj["formatted_price"]:
- price_obj["formatted_price"] = ""
+ price_obj["formatted_price"], price_obj["formatted_mrp"] = "", ""
return price_obj
def get_non_stock_item_status(item_code, item_warehouse_field):
- #if item belongs to product bundle, check if bundle items are in stock
+ # if item is a product bundle, check if its bundle items are in stock
if frappe.db.exists("Product Bundle", item_code):
items = frappe.get_doc("Product Bundle", item_code).get_all_children()
bundle_warehouse = frappe.db.get_value('Item', item_code, item_warehouse_field)
- return all(get_qty_in_stock(d.item_code, item_warehouse_field, bundle_warehouse).in_stock for d in items)
+ return all([get_web_item_qty_in_stock(d.item_code, item_warehouse_field, bundle_warehouse).in_stock for d in items])
else:
return 1
diff --git a/erpnext/www/all-products/img/placeholder.png b/erpnext/www/all-products/img/placeholder.png
new file mode 100644
index 0000000..9780ad8
--- /dev/null
+++ b/erpnext/www/all-products/img/placeholder.png
Binary files differ
diff --git a/erpnext/www/all-products/index.html b/erpnext/www/all-products/index.html
index a7838ee..1e9b482 100644
--- a/erpnext/www/all-products/index.html
+++ b/erpnext/www/all-products/index.html
@@ -1,4 +1,6 @@
+{% from "erpnext/templates/includes/macros.html" import attribute_filter_section, field_filter_section, discount_range_filters %}
{% extends "templates/web.html" %}
+
{% block title %}{{ _('Products') }}{% endblock %}
{% block header %}
<div class="mb-6">{{ _('Products') }}</div>
@@ -53,68 +55,20 @@
<div class="mb-4 filters-title" > {{ _('Filters') }} </div>
<a class="mb-4 clear-filters" href="/all-products">{{ _('Clear All') }}</a>
</div>
- {% for field_filter in field_filters %}
- {%- set item_field = field_filter[0] %}
- {%- set values = field_filter[1] %}
- <div class="mb-4 filter-block pb-5">
- <div class="filter-label mb-3">{{ item_field.label }}</div>
+ <!-- field filters -->
+ {% if field_filters %}
+ {{ field_filter_section(field_filters) }}
+ {% endif %}
- {% if values | len > 20 %}
- <!-- show inline filter if values more than 20 -->
- <input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
- {% endif %}
+ <!-- attribute filters -->
+ {% if attribute_filters %}
+ {{ attribute_filter_section(attribute_filters) }}
+ {% endif %}
- {% if values %}
- <div class="filter-options">
- {% for value in values %}
- <div class="checkbox" data-value="{{ value }}">
- <label for="{{value}}">
- <input type="checkbox"
- class="product-filter field-filter"
- id="{{value}}"
- data-filter-name="{{ item_field.fieldname }}"
- data-filter-value="{{ value }}"
- >
- <span class="label-area">{{ value }}</span>
- </label>
- </div>
- {% endfor %}
- </div>
- {% else %}
- <i class="text-muted">{{ _('No values') }}</i>
- {% endif %}
- </div>
- {% endfor %}
-
- {% for attribute in attribute_filters %}
- <div class="mb-4 filter-block pb-5">
- <div class="filter-label mb-3">{{ attribute.name}}</div>
- {% if values | len > 20 %}
- <!-- show inline filter if values more than 20 -->
- <input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
- {% endif %}
-
- {% if attribute.item_attribute_values %}
- <div class="filter-options">
- {% for attr_value in attribute.item_attribute_values %}
- <div class="checkbox">
- <label>
- <input type="checkbox"
- class="product-filter attribute-filter"
- id="{{attr_value}}"
- data-attribute-name="{{ attribute.name }}"
- data-attribute-value="{{ attr_value }}"
- {% if attr_value.checked %} checked {% endif %}>
- <span class="label-area">{{ attr_value }}</span>
- </label>
- </div>
- {% endfor %}
- </div>
- {% else %}
- <i class="text-muted">{{ _('No values') }}</i>
- {% endif %}
- </div>
- {% endfor %}
+ <!-- discount filters -->
+ {% if discount_filters %}
+ {{ discount_range_filters(discount_filters) }}
+ {% endif %}
</div>
<script>
diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js
index 1c641b5..d2a3b19 100644
--- a/erpnext/www/all-products/index.js
+++ b/erpnext/www/all-products/index.js
@@ -2,6 +2,7 @@
class ProductListing {
constructor() {
this.bind_filters();
+ this.bind_card_actions();
this.bind_search();
this.restore_filters_state();
}
@@ -31,12 +32,16 @@
if (this.attribute_filters[attribute_name].length === 0) {
delete this.attribute_filters[attribute_name];
}
- } else if ($checkbox.is('.field-filter')) {
+ } else if ($checkbox.is('.field-filter') || $checkbox.is('.discount-filter')) {
const {
filterName: filter_name,
filterValue: filter_value
} = $checkbox.data();
+ if ($checkbox.is('.discount-filter')) {
+ // clear previous discount filter to accomodate new
+ delete this.field_filters["discount"];
+ }
if (is_checked) {
this.field_filters[filter_name] = this.field_filters[filter_name] || [];
this.field_filters[filter_name].push(filter_value);
@@ -71,8 +76,9 @@
}, 1000));
}
- make_filters() {
-
+ bind_card_actions() {
+ erpnext.shopping_cart.bind_add_to_cart_action();
+ erpnext.wishlist.bind_wishlist_action();
}
bind_search() {
@@ -129,7 +135,7 @@
Object.assign(field_filters, { item_group });
}
return new Promise((resolve, reject) => {
- frappe.call('erpnext.portal.product_configurator.utils.get_products_html_for_website', args)
+ frappe.call('erpnext.www.all-products.index.get_products_html_for_website', args)
.then(r => {
if (r.exc) reject(r.exc);
else resolve(r.message);
diff --git a/erpnext/www/all-products/index.py b/erpnext/www/all-products/index.py
index df5258b..a4662bb 100644
--- a/erpnext/www/all-products/index.py
+++ b/erpnext/www/all-products/index.py
@@ -1,8 +1,7 @@
import frappe
-
-from erpnext.portal.product_configurator.utils import get_product_settings
-from erpnext.shopping_cart.filters import ProductFiltersBuilder
-from erpnext.shopping_cart.product_query import ProductQuery
+from frappe.utils import cint
+from erpnext.e_commerce.product_query import ProductQuery
+from erpnext.e_commerce.filters import ProductFiltersBuilder
sitemap = 1
@@ -12,25 +11,47 @@
search = frappe.form_dict.search
field_filters = frappe.parse_json(frappe.form_dict.field_filters)
attribute_filters = frappe.parse_json(frappe.form_dict.attribute_filters)
- start = frappe.parse_json(frappe.form_dict.start)
+ start = cint(frappe.parse_json(frappe.form_dict.start))
else:
search = field_filters = attribute_filters = None
start = 0
engine = ProductQuery()
- context.items = engine.query(attribute_filters, field_filters, search, start)
+ context.items, discounts = engine.query(attribute_filters, field_filters, search, start)
# Add homepage as parent
context.parents = [{"name": frappe._("Home"), "route":"/"}]
- product_settings = get_product_settings()
filter_engine = ProductFiltersBuilder()
context.field_filters = filter_engine.get_field_filters()
context.attribute_filters = filter_engine.get_attribute_filters()
+ if discounts:
+ context.discount_filters = filter_engine.get_discount_filters(discounts)
- context.product_settings = product_settings
- context.body_class = "product-page"
- context.page_length = product_settings.products_per_page or 20
+ context.e_commerce_settings = engine.settings
+ context.page_length = engine.settings.products_per_page or 20
context.no_cache = 1
+
+@frappe.whitelist(allow_guest=True)
+def get_products_html_for_website(field_filters=None, attribute_filters=None):
+ """Get Products on filter change."""
+ field_filters = frappe.parse_json(field_filters)
+ attribute_filters = frappe.parse_json(attribute_filters)
+
+ engine = ProductQuery()
+ items, discounts = engine.query(attribute_filters, field_filters, search_term=None, start=0)
+
+ item_html = []
+ for item in items:
+ item_html.append(frappe.render_template('erpnext/www/all-products/item_row.html', {
+ 'item': item,
+ 'e_commerce_settings': None
+ }))
+ html = ''.join(item_html)
+
+ if not items:
+ html = frappe.render_template('erpnext/www/all-products/not_found.html', {})
+
+ return html
diff --git a/erpnext/www/all-products/item_row.html b/erpnext/www/all-products/item_row.html
index a7e994c..538ce3b 100644
--- a/erpnext/www/all-products/item_row.html
+++ b/erpnext/www/all-products/item_row.html
@@ -1,6 +1,4 @@
{% from "erpnext/templates/includes/macros.html" import item_card, item_card_body %}
-{{ item_card(
- item.item_name or item.name, item.website_image or item.image, item.route, item.website_description or item.description,
- item.formatted_price, item.item_group
-) }}
+{{ item_card(item, e_commerce_settings) }}
+
diff --git a/erpnext/www/all-products/search.css b/erpnext/www/all-products/search.css
new file mode 100644
index 0000000..687532d
--- /dev/null
+++ b/erpnext/www/all-products/search.css
@@ -0,0 +1,9 @@
+.item-thumb {
+ height: 50px;
+ width: 50px;
+ object-fit: cover;
+}
+
+.brand-line {
+ color: gray;
+}
\ No newline at end of file
diff --git a/erpnext/www/all-products/search.html b/erpnext/www/all-products/search.html
new file mode 100644
index 0000000..735822d
--- /dev/null
+++ b/erpnext/www/all-products/search.html
@@ -0,0 +1,46 @@
+{% extends "templates/web.html" %}
+
+{% block title %}{{ _('Search') }}{% endblock %}
+
+{%- block head_include %}
+ <link rel="stylesheet" href="search.css">
+{% endblock -%}
+
+{% block header %}
+<div class="mb-6">{{ _('Search Products') }}</div>
+{% endblock header %}
+
+{% block page_content %}
+<div class="input-group mb-3">
+ <input type="text" name="query" id="search-box" class="form-control" placeholder="Search for products..." aria-label="Product" aria-describedby="button-addon2">
+ <div class="input-group-append">
+ <button class="btn btn-outline-secondary" type="button" id="search-button">Search</button>
+ </div>
+</div>
+
+<!-- To show recent searches -->
+<div class="my-2" id="recent-search-chips"></div>
+
+<div class="row mt-2">
+ <!-- Search Results -->
+ <div class="col-sm">
+ <h2>Products</h2>
+ <ul id="results" class="list-group"></ul>
+ </div>
+
+ {% set show_categories = frappe.db.get_single_value('E Commerce Settings', 'show_categories_in_search_autocomplete') %}
+ {% if show_categories %}
+ <div id="categories" class="col-sm">
+ <h2>Categories</h2>
+ <ul id="category-suggestions">
+ </ul>
+ </div>
+ {% endif %}
+
+ {% set show_brand_line = frappe.db.get_single_value('E Commerce Settings', 'show_brand_line') %}
+ {% if show_brand_line %}
+ <span id="show-brand-line"></span>
+ {% endif %}
+</div>
+
+{% endblock %}
\ No newline at end of file
diff --git a/erpnext/www/all-products/search.js b/erpnext/www/all-products/search.js
new file mode 100644
index 0000000..cb3f9af
--- /dev/null
+++ b/erpnext/www/all-products/search.js
@@ -0,0 +1,131 @@
+let loading = false;
+
+const MAX_RECENT_SEARCHES = 4;
+
+const searchBox = document.getElementById("search-box");
+const searchButton = document.getElementById("search-button");
+const results = document.getElementById("results");
+const categoryList = document.getElementById("category-suggestions");
+const showBrandLine = document.getElementById("show-brand-line");
+const recentSearchArea = document.getElementById("recent-search-chips");
+
+function getRecentSearches() {
+ return JSON.parse(localStorage.getItem("recent_searches") || "[]");
+}
+
+function attachEventListenersToChips() {
+ const chips = document.getElementsByClassName("recent-chip");
+
+ for (let chip of chips) {
+ chip.addEventListener("click", () => {
+ searchBox.value = chip.innerText;
+
+ // Start search with `recent query`
+ const event = new Event("input");
+ searchBox.dispatchEvent(event);
+ searchBox.focus();
+ });
+ }
+}
+
+function populateRecentSearches() {
+ let recents = getRecentSearches();
+
+ if (!recents.length) {
+ return;
+ }
+
+ html = "Recent Searches: ";
+ for (let query of recents) {
+ html += `<button class="btn btn-secondary btn-sm recent-chip mr-1">${query}</button>`;
+ }
+
+ recentSearchArea.innerHTML = html;
+ attachEventListenersToChips();
+}
+
+function populateResults(data) {
+ html = ""
+ for (let res of data.message) {
+ html += `<li class="list-group-item list-group-item-action">
+ <img class="item-thumb" src="${res.thumbnail || 'img/placeholder.png'}" />
+ <a href="/${res.route}">${res.web_item_name} <span class="brand-line">${showBrandLine && res.brand ? "by " + res.brand : ""}</span></a>
+ </li>`
+ }
+ results.innerHTML = html;
+}
+
+function populateCategoriesList(data) {
+ if (data.length === 0) {
+ categoryList.innerHTML = 'Type something...';
+ return;
+ }
+
+ html = ""
+ for (let category of data.message) {
+ html += `<li>${category}</li>`
+ }
+
+ categoryList.innerHTML = html;
+}
+
+function updateLoadingState() {
+ if (loading) {
+ results.innerHTML = `<div class="spinner-border"><span class="sr-only">loading...<span></div>`;
+ }
+}
+
+searchBox.addEventListener("input", (e) => {
+ loading = true;
+ updateLoadingState();
+ frappe.call({
+ method: "erpnext.templates.pages.product_search.search",
+ args: {
+ query: e.target.value
+ },
+ callback: (data) => {
+ populateResults(data);
+ loading = false;
+ }
+ });
+
+ // If there is a suggestion list node
+ if (categoryList) {
+ frappe.call({
+ method: "erpnext.templates.pages.product_search.get_category_suggestions",
+ args: {
+ query: e.target.value
+ },
+ callback: (data) => {
+ populateCategoriesList(data);
+ }
+ });
+ }
+});
+
+searchButton.addEventListener("click", (e) => {
+ let query = searchBox.value;
+ if (!query) {
+ return;
+ }
+
+ let recents = getRecentSearches();
+
+ if (recents.length >= MAX_RECENT_SEARCHES) {
+ // Remove the `First` query
+ recents.splice(0, 1);
+ }
+
+ if (recents.indexOf(query) >= 0) {
+ return;
+ }
+
+ recents.push(query);
+
+ localStorage.setItem("recent_searches", JSON.stringify(recents));
+
+ // Refresh recent searches
+ populateRecentSearches();
+});
+
+populateRecentSearches();
\ No newline at end of file
diff --git a/erpnext/portal/product_configurator/__init__.py b/erpnext/www/shop-by-category/__init__.py
similarity index 100%
copy from erpnext/portal/product_configurator/__init__.py
copy to erpnext/www/shop-by-category/__init__.py
diff --git a/erpnext/www/shop-by-category/category_card_section.html b/erpnext/www/shop-by-category/category_card_section.html
new file mode 100644
index 0000000..56cb63a
--- /dev/null
+++ b/erpnext/www/shop-by-category/category_card_section.html
@@ -0,0 +1,30 @@
+{%- macro card(title, image, type, url=None, text_primary=False) -%}
+<!-- style defined at shop-by-category index -->
+<div class="card category-card" data-type="{{ type }}" data-name="{{ title }}">
+ {% if image %}
+ <img class="card-img-top" src="{{ image }}" alt="{{ title }}" style="height: 80%;">
+ {% else %}
+ <div class="placeholder-div">
+ <span class="placeholder">
+ {{ frappe.utils.get_abbr(title) }}
+ </span>
+ </div>
+ {% endif %}
+ <div class="card-body text-center text-muted">
+ {{ title or '' }}
+ </div>
+ <a href="{{ url or '#' }}" class="stretched-link"></a>
+</div>
+{%- endmacro -%}
+
+<div class="col-12 item-card-group-section">
+ <div class="row products-list product-category-section">
+ {%- for row in data -%}
+ {%- set title = row.name -%}
+ {%- set image = row.get("image") -%}
+ {%- if title -%}
+ {{ card(title, image, type, row.get("route")) }}
+ {%- endif -%}
+ {%- endfor -%}
+ </div>
+</div>
\ No newline at end of file
diff --git a/erpnext/www/shop-by-category/index.html b/erpnext/www/shop-by-category/index.html
new file mode 100644
index 0000000..ac0b317
--- /dev/null
+++ b/erpnext/www/shop-by-category/index.html
@@ -0,0 +1,60 @@
+{% extends "templates/web.html" %}
+{% block title %}{{ _('Shop by Category') }}{% endblock %}
+
+{% block head_include %}
+<style>
+ .category-slideshow {
+ margin-bottom: 2rem;
+ }
+ .category-card {
+ height: 300px !important;
+ width: 300px !important;
+ margin: 30px !important;
+ }
+ .placeholder-div {
+ height:80%;
+ width: -webkit-fill-available;
+ padding: 50px;
+ text-align: center;
+ background-color: #F9FAFA;
+ border-top-left-radius: calc(0.75rem - 1px);
+ border-top-right-radius: calc(0.75rem - 1px);
+ }
+ .placeholder {
+ font-size: 72px;
+ }
+</style>
+{% endblock %}
+
+{% block script %}
+<script type="text/javascript" src="/shop-by-category/index.js"></script>
+{% endblock %}
+
+{% block page_content %}
+<div class="shop-by-category-content">
+ <div class="category-slideshow">
+ {% if slideshow %}
+ <!-- slideshow -->
+ {{ web_block(
+ "Hero Slider",
+ values=slideshow,
+ add_container=0,
+ add_top_padding=0,
+ add_bottom_padding=0,
+ ) }}
+ {% endif %}
+ </div>
+ <div class="category-tabs">
+ {% if tabs %}
+ <!-- tabs -->
+ {{ web_block(
+ "Section with Tabs",
+ values=tabs,
+ add_container=0,
+ add_top_padding=0,
+ add_bottom_padding=0
+ ) }}
+ {% endif %}
+ </div>
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/erpnext/www/shop-by-category/index.js b/erpnext/www/shop-by-category/index.js
new file mode 100644
index 0000000..1b3116f
--- /dev/null
+++ b/erpnext/www/shop-by-category/index.js
@@ -0,0 +1,12 @@
+$(() => {
+ $('.category-card').on('click', (e) => {
+ let category_type = e.currentTarget.dataset.type;
+ let category_name = e.currentTarget.dataset.name;
+
+ if (category_type != "item_group") {
+ let filters = {};
+ filters[category_type] = [category_name];
+ window.location.href = "/all-products?field_filters=" + JSON.stringify(filters);
+ }
+ });
+});
\ No newline at end of file
diff --git a/erpnext/www/shop-by-category/index.py b/erpnext/www/shop-by-category/index.py
new file mode 100644
index 0000000..c295335
--- /dev/null
+++ b/erpnext/www/shop-by-category/index.py
@@ -0,0 +1,73 @@
+import frappe
+from frappe import _
+
+sitemap = 1
+
+def get_context(context):
+ settings = frappe.get_doc("E Commerce Settings")
+ context.categories_enabled = settings.enable_field_filters
+
+ if context.categories_enabled:
+ categories = [row.fieldname for row in settings.filter_fields]
+ context.tabs = get_tabs(categories)
+
+ if settings.slideshow:
+ context.slideshow = get_slideshow(settings.slideshow)
+
+ context.no_cache = 1
+
+def get_slideshow(slideshow):
+ values = {
+ 'show_indicators': 1,
+ 'show_controls': 1,
+ 'rounded': 1,
+ 'slider_name': "Categories"
+ }
+ slideshow = frappe.get_doc("Website Slideshow", slideshow)
+ slides = slideshow.get({"doctype": "Website Slideshow Item"})
+ for index, slide in enumerate(slides):
+ values[f"slide_{index + 1}_image"] = slide.image
+ values[f"slide_{index + 1}_title"] = slide.heading
+ values[f"slide_{index + 1}_subtitle"] = slide.description
+ values[f"slide_{index + 1}_theme"] = slide.get("theme") or "Light"
+ values[f"slide_{index + 1}_content_align"] = slide.get("content_align") or "Centre"
+ values[f"slide_{index + 1}_primary_action"] = slide.url
+
+ return values
+
+def get_tabs(categories):
+ tab_values = {
+ 'title': _("Shop by Category"),
+ }
+
+ categorical_data = get_category_records(categories)
+ for index, tab in enumerate(categorical_data):
+ tab_values[f"tab_{index + 1}_title"] = frappe.unscrub(tab)
+ # pre-render cards for each tab
+ tab_values[f"tab_{index + 1}_content"] = frappe.render_template(
+ "erpnext/www/shop-by-category/category_card_section.html",
+ {"data": categorical_data[tab], "type": tab}
+ )
+ return tab_values
+
+def get_category_records(categories):
+ categorical_data = {}
+ for category in categories:
+ if category == "item_group":
+ categorical_data["item_group"] = frappe.db.sql("""
+ Select name, parent_item_group, is_group, image, route
+ from `tabItem Group`
+ where parent_item_group='All Item Groups'
+ and show_in_website=1""", as_dict=1)
+ else:
+ doctype = frappe.unscrub(category)
+ fields = ["name"]
+ if frappe.get_meta(doctype, cached=True).get_field("image"):
+ fields += ["image"]
+
+ categorical_data[category] = frappe.db.sql("""
+ Select {fields}
+ from `tab{doctype}`""".format(doctype=doctype, fields=",".join(fields)), as_dict=1)
+
+ return categorical_data
+
diff --git a/requirements.txt b/requirements.txt
index f28906a..400e6a3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,3 +11,4 @@
taxjar~=1.9.2
tweepy~=3.10.0
Unidecode~=1.2.0
+redisearch==2.0.0
\ No newline at end of file