Merge branch 'develop' into provisonal_loss_bs
diff --git a/erpnext/accounts/doctype/cost_center_allocation_percentage/cost_center_allocation_percentage.json b/erpnext/accounts/doctype/cost_center_allocation_percentage/cost_center_allocation_percentage.json
index 4b871ae..7e50962 100644
--- a/erpnext/accounts/doctype/cost_center_allocation_percentage/cost_center_allocation_percentage.json
+++ b/erpnext/accounts/doctype/cost_center_allocation_percentage/cost_center_allocation_percentage.json
@@ -1,7 +1,7 @@
 {
  "actions": [],
  "allow_rename": 1,
- "creation": "2022-01-03 18:10:11.697198",
+ "creation": "2022-01-13 20:07:30.096306",
  "doctype": "DocType",
  "editable_grid": 1,
  "engine": "InnoDB",
@@ -20,7 +20,7 @@
   },
   {
    "fieldname": "percentage",
-   "fieldtype": "Int",
+   "fieldtype": "Percent",
    "in_list_view": 1,
    "label": "Percentage (%)",
    "reqd": 1
@@ -29,7 +29,7 @@
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2022-01-03 18:10:20.029821",
+ "modified": "2022-02-01 22:22:31.589523",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Cost Center Allocation Percentage",
diff --git a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py
index 5ba0691..ca482c8 100644
--- a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py
+++ b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py
@@ -39,9 +39,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 6a84a65..d72d8f7 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -291,7 +291,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
@@ -435,13 +435,13 @@
 	""", (ref_dt, ref_dn))
 	return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
 
-def get_gateway_details(args):
+def get_gateway_details(args): # nosemgrep
 	"""return gateway and payment account of default payment gateway"""
 	if args.get("payment_gateway_account"):
 		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/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index f66abdc..97d34e0 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -42,7 +42,6 @@
 		self.validate_serialised_or_batched_item()
 		self.validate_stock_availablility()
 		self.validate_return_items_qty()
-		self.validate_non_stock_items()
 		self.set_status()
 		self.set_account_for_mode_of_payment()
 		self.validate_pos()
@@ -175,9 +174,11 @@
 	def validate_stock_availablility(self):
 		if self.is_return or self.docstatus != 1:
 			return
-
 		allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
 		for d in self.get('items'):
+			is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item'))
+			if is_service_item:
+				return
 			if d.serial_no:
 				self.validate_pos_reserved_serial_nos(d)
 				self.validate_delivered_serial_nos(d)
@@ -188,7 +189,7 @@
 				if allow_negative_stock:
 					return
 
-				available_stock = get_stock_availability(d.item_code, d.warehouse)
+				available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)
 
 				item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
 				if flt(available_stock) <= 0:
@@ -259,14 +260,6 @@
 							.format(d.idx, bold_serial_no, bold_return_against)
 						)
 
-	def validate_non_stock_items(self):
-		for d in self.get("items"):
-			is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
-			if not is_stock_item:
-				if not frappe.db.exists('Product Bundle', d.item_code):
-					frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice.")
-						.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
-
 	def validate_mode_of_payment(self):
 		if len(self.payments) == 0:
 			frappe.throw(_("At least one mode of payment is required for POS invoice."))
@@ -506,12 +499,18 @@
 @frappe.whitelist()
 def get_stock_availability(item_code, warehouse):
 	if frappe.db.get_value('Item', item_code, 'is_stock_item'):
+		is_stock_item = True
 		bin_qty = get_bin_qty(item_code, warehouse)
 		pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
-		return bin_qty - pos_sales_qty
+		return bin_qty - pos_sales_qty, is_stock_item
 	else:
+		is_stock_item = False
 		if frappe.db.exists('Product Bundle', item_code):
-			return get_bundle_availability(item_code, warehouse)
+			return get_bundle_availability(item_code, warehouse), is_stock_item
+		else:
+			# Is a service item
+			return 0, is_stock_item
+
 
 def get_bundle_availability(bundle_item_code, warehouse):
 	product_bundle = frappe.get_doc('Product Bundle', bundle_item_code)
diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.py b/erpnext/accounts/doctype/tax_rule/tax_rule.py
index a16377c..2d94bc3 100644
--- a/erpnext/accounts/doctype/tax_rule/tax_rule.py
+++ b/erpnext/accounts/doctype/tax_rule/tax_rule.py
@@ -98,7 +98,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 14d2ccd..4f9ff43 100644
--- a/erpnext/buying/doctype/supplier/supplier.py
+++ b/erpnext/buying/doctype/supplier/supplier.py
@@ -131,28 +131,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 2bad6f8..68ad702 100644
--- a/erpnext/controllers/item_variant.py
+++ b/erpnext/controllers/item_variant.py
@@ -132,7 +132,7 @@
 
 	conditions = " or ".join(conditions)
 
-	from erpnext.portal.product_configurator.utils import get_item_codes_by_attributes
+	from erpnext.e_commerce.variant_selector.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:
@@ -262,9 +262,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/controllers/queries.py b/erpnext/controllers/queries.py
index dc04dab..902e115 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -249,6 +249,9 @@
 				del filters['customer']
 			else:
 				del filters['supplier']
+		else:
+			filters.pop('customer', None)
+			filters.pop('supplier', None)
 
 
 	description_cond = ''
diff --git a/erpnext/controllers/tests/test_queries.py b/erpnext/controllers/tests/test_queries.py
index 908d78c..60d1733 100644
--- a/erpnext/controllers/tests/test_queries.py
+++ b/erpnext/controllers/tests/test_queries.py
@@ -56,6 +56,12 @@
 		bundled_stock_items = query(txt="_test product bundle item 5", filters={"is_stock_item": 1})
 		self.assertEqual(len(bundled_stock_items), 0)
 
+		# empty customer/supplier should be stripped of instead of failure
+		query(txt="", filters={"customer": None})
+		query(txt="", filters={"customer": ""})
+		query(txt="", filters={"supplier": None})
+		query(txt="", filters={"supplier": ""})
+
 	def test_bom_qury(self):
 		query = add_default_params(queries.bom, "BOM")
 
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/e_commerce/api.py b/erpnext/e_commerce/api.py
new file mode 100644
index 0000000..43cb36c
--- /dev/null
+++ b/erpnext/e_commerce/api.py
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import json
+
+import frappe
+from frappe.utils import cint
+
+from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder
+from erpnext.e_commerce.product_data_engine.query import ProductQuery
+from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website
+
+
+@frappe.whitelist(allow_guest=True)
+def get_product_filter_data(query_args=None):
+	"""
+		Returns filtered products and discount filters.
+		:param query_args (dict): contains filters to get products list
+
+		Query Args filters:
+		search (str): Search Term.
+		field_filters (dict): Keys include item_group, brand, etc.
+		attribute_filters(dict): Keys include Color, Size, etc.
+		start (int): Offset items by
+		item_group (str): Valid Item Group
+		from_filters (bool): Set as True to jump to page 1
+	"""
+	if isinstance(query_args, str):
+		query_args = json.loads(query_args)
+
+	query_args = frappe._dict(query_args)
+	if query_args:
+		search = query_args.get("search")
+		field_filters = query_args.get("field_filters", {})
+		attribute_filters = query_args.get("attribute_filters", {})
+		start = cint(query_args.start) if query_args.get("start") else 0
+		item_group = query_args.get("item_group")
+		from_filters = query_args.get("from_filters")
+	else:
+		search, attribute_filters, item_group, from_filters = None, None, None, None
+		field_filters = {}
+		start = 0
+
+	# if new filter is checked, reset start to show filtered items from page 1
+	if from_filters:
+		start = 0
+
+	sub_categories = []
+	if item_group:
+		field_filters['item_group'] = item_group
+		sub_categories = get_child_groups_for_website(item_group, immediate=True)
+
+	engine = ProductQuery()
+	try:
+		result = engine.query(
+			attribute_filters,
+			field_filters,
+			search_term=search,
+			start=start,
+			item_group=item_group
+		)
+	except Exception:
+		traceback = frappe.get_traceback()
+		frappe.log_error(traceback, frappe._("Product Engine Error"))
+		return {"exc": "Something went wrong!"}
+
+	# discount filter data
+	filters = {}
+	discounts = result["discounts"]
+
+	if discounts:
+		filter_engine = ProductFiltersBuilder()
+		filters["discount_filters"] = filter_engine.get_discount_filters(discounts)
+
+	return {
+		"items": result["items"] or [],
+		"filters": filters,
+		"settings": engine.settings,
+		"sub_categories": sub_categories,
+		"items_count": result["items_count"]
+	}
+
+@frappe.whitelist(allow_guest=True)
+def get_guest_redirect_on_action():
+	return frappe.db.get_single_value("E Commerce Settings", "redirect_on_action")
\ No newline at end of file
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/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js
new file mode 100644
index 0000000..6302d26
--- /dev/null
+++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js
@@ -0,0 +1,53 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+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;
+			frm.refresh_field("quotation_series");
+		}
+
+		frm.set_query('payment_gateway_account', function() {
+			return { 'filters': { 'payment_channel': "Email" } };
+		});
+	},
+	refresh: function(frm) {
+		if (frm.doc.enabled) {
+			frm.get_field('store_page_docs').$wrapper.removeClass('hide-control').html(
+				`<div>${__("Follow these steps to create a landing page for your store")}:
+					<a href="https://docs.erpnext.com/docs/user/manual/en/website/store-landing-page"
+						style="color: var(--gray-600)">
+						docs/store-landing-page
+					</a>
+				</div>`
+			);
+		}
+
+		frappe.model.with_doctype("Item", () => {
+			const web_item_meta = frappe.get_meta('Website Item');
+
+			const valid_fields = web_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) {
+			frm.set_value('enable_variants', 1);
+		}
+		else {
+			frm.set_value('company', '');
+			frm.set_value('price_list', '');
+			frm.set_value('default_customer_group', '');
+			frm.set_value('quotation_series', '');
+		}
+	}
+});
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..d5fb969
--- /dev/null
+++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json
@@ -0,0 +1,393 @@
+{
+ "actions": [],
+ "creation": "2021-02-10 17:13:39.139103",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "products_per_page",
+  "filter_categories_section",
+  "enable_field_filters",
+  "filter_fields",
+  "enable_attribute_filters",
+  "filter_attributes",
+  "display_settings_section",
+  "hide_variants",
+  "enable_variants",
+  "show_price",
+  "column_break_9",
+  "show_stock_availability",
+  "show_quantity_in_website",
+  "allow_items_not_in_stock",
+  "column_break_13",
+  "show_apply_coupon_code_in_website",
+  "show_contact_us_button",
+  "show_attachments",
+  "section_break_18",
+  "company",
+  "price_list",
+  "enabled",
+  "store_page_docs",
+  "column_break_21",
+  "default_customer_group",
+  "quotation_series",
+  "checkout_settings_section",
+  "enable_checkout",
+  "show_price_in_quotation",
+  "column_break_27",
+  "save_quotations_as_draft",
+  "payment_gateway_account",
+  "payment_success_url",
+  "add_ons_section",
+  "enable_wishlist",
+  "column_break_22",
+  "enable_reviews",
+  "column_break_23",
+  "enable_recommendations",
+  "item_search_settings_section",
+  "redisearch_warning",
+  "search_index_fields",
+  "show_categories_in_search_autocomplete",
+  "is_redisearch_loaded",
+  "shop_by_category_section",
+  "slideshow",
+  "guest_display_settings_section",
+  "hide_price_for_guest",
+  "redirect_on_action"
+ ],
+ "fields": [
+  {
+   "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"
+  },
+  {
+   "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": "Website 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 Variant Selection"
+  },
+  {
+   "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"
+  },
+  {
+   "default": "0",
+   "fieldname": "enable_reviews",
+   "fieldtype": "Check",
+   "label": "Enable Reviews and Ratings"
+  },
+  {
+   "fieldname": "search_index_fields",
+   "fieldtype": "Small Text",
+   "label": "Search Index Fields",
+   "read_only_depends_on": "eval:!doc.is_redisearch_loaded"
+  },
+  {
+   "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",
+   "read_only_depends_on": "eval:!doc.is_redisearch_loaded"
+  },
+  {
+   "default": "0",
+   "fieldname": "is_redisearch_loaded",
+   "fieldtype": "Check",
+   "hidden": 1,
+   "label": "Is Redisearch Loaded"
+  },
+  {
+   "depends_on": "eval:!doc.is_redisearch_loaded",
+   "fieldname": "redisearch_warning",
+   "fieldtype": "HTML",
+   "label": "Redisearch Warning",
+   "options": "<p class=\"alert alert-warning\">Redisearch is not loaded. If you want to use the advanced product search feature, refer <a class=\"alert-link\" href=\"https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/articles/installing-redisearch\" target=\"_blank\">here</a>.</p>"
+  },
+  {
+   "default": "0",
+   "depends_on": "eval:doc.show_price",
+   "fieldname": "hide_price_for_guest",
+   "fieldtype": "Check",
+   "label": "Hide Price for Guest"
+  },
+  {
+   "fieldname": "column_break_9",
+   "fieldtype": "Column Break"
+  },
+  {
+   "collapsible": 1,
+   "fieldname": "guest_display_settings_section",
+   "fieldtype": "Section Break",
+   "label": "Guest Display Settings"
+  },
+  {
+   "description": "Link to redirect Guest on actions that need login such as add to cart, wishlist, etc. <b>E.g.: /login</b>",
+   "fieldname": "redirect_on_action",
+   "fieldtype": "Data",
+   "label": "Redirect on Action"
+  },
+  {
+   "fieldname": "column_break_22",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "column_break_23",
+   "fieldtype": "Column Break"
+  },
+  {
+   "default": "0",
+   "fieldname": "enable_recommendations",
+   "fieldtype": "Check",
+   "label": "Enable Recommendations"
+  },
+  {
+   "default": "0",
+   "depends_on": "eval: doc.enable_checkout == 0",
+   "fieldname": "show_price_in_quotation",
+   "fieldtype": "Check",
+   "label": "Show Price in Quotation"
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2021-09-02 14:02:44.785824",
+ "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..dd7b114
--- /dev/null
+++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py
@@ -0,0 +1,151 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import comma_and, flt, unique
+
+from erpnext.e_commerce.redisearch_utils import (
+	create_website_items_index,
+	get_indexable_web_fields,
+	is_search_module_loaded,
+)
+
+
+class ShoppingCartSetupError(frappe.ValidationError): pass
+
+class ECommerceSettings(Document):
+	def onload(self):
+		self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series")
+		self.is_redisearch_loaded = is_search_module_loaded()
+
+	def validate(self):
+		self.validate_field_filters()
+		self.validate_attribute_filters()
+		self.validate_checkout()
+		self.validate_search_index_fields()
+
+		if self.enabled:
+			self.validate_price_list_exchange_rate()
+
+		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
+
+		fields = self.search_index_fields.replace(' ', '')
+		fields = unique(fields.strip(',').split(',')) # Remove extra ',' and remove duplicates
+
+		# All fields should be indexable
+		allowed_indexable_fields = get_indexable_web_fields()
+
+		if not (set(fields).issubset(allowed_indexable_fields)):
+			invalid_fields = list(set(fields).difference(allowed_indexable_fields))
+			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_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 on_change(self):
+		old_doc = self.get_doc_before_save()
+
+		if old_doc:
+			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=None, method=None):
+	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
diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py
similarity index 67%
rename from erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py
rename to erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py
index c3809b3..86cef30 100644
--- a/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py
+++ b/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py
@@ -1,24 +1,21 @@
-# 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
-
-
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
 import unittest
 
 import frappe
 
-from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
+from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
 	ShoppingCartSetupError,
 )
 
 
-class TestShoppingCartSettings(unittest.TestCase):
+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": "Shopping Cart Settings",
+		return frappe.get_doc({"doctype": "E Commerce Settings",
 			"company": "_Test Company"})
 
 	# NOTE: Exchangrate API has all enabled currencies that ERPNext supports.
@@ -34,15 +31,17 @@
 
 	# 	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)
+	# 	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
+	# 	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()
+	# 	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() # nosemgrep
 
 		cart_settings = self.get_cart_settings()
 		cart_settings.enabled = 1
@@ -51,4 +50,13 @@
 
 		frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1")
 
+def setup_e_commerce_settings(values_dict):
+	"Accepts a dict of values that updates E Commerce Settings."
+	if not values_dict:
+		return
+
+	doc = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
+	doc.update(values_dict)
+	doc.save()
+
 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..57f719f
--- /dev/null
+++ b/erpnext/e_commerce/doctype/item_review/item_review.json
@@ -0,0 +1,134 @@
+{
+ "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",
+   "read_only": 1,
+   "reqd": 1
+  },
+  {
+   "fieldname": "user",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "User",
+   "options": "User",
+   "read_only": 1
+  },
+  {
+   "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",
+   "read_only": 1
+  },
+  {
+   "fieldname": "comment",
+   "fieldtype": "Small Text",
+   "label": "Comment",
+   "read_only": 1
+  },
+  {
+   "fieldname": "review_title",
+   "fieldtype": "Data",
+   "label": "Review Title",
+   "read_only": 1
+  },
+  {
+   "fieldname": "customer",
+   "fieldtype": "Link",
+   "label": "Customer",
+   "options": "Customer",
+   "read_only": 1
+  },
+  {
+   "fieldname": "published_on",
+   "fieldtype": "Data",
+   "label": "Published on",
+   "read_only": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-08-10 12:08:58.119691",
+ "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..966ec35
--- /dev/null
+++ b/erpnext/e_commerce/doctype/item_review/item_review.py
@@ -0,0 +1,147 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from datetime import datetime
+
+import frappe
+from frappe import _
+from frappe.contacts.doctype.contact.contact import get_contact_name
+from frappe.model.document import Document
+from frappe.utils import cint, flt
+
+from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
+	get_shopping_cart_settings,
+)
+
+
+class UnverifiedReviewer(frappe.ValidationError):
+	pass
+
+class ItemReview(Document):
+	def after_insert(self):
+		# regenerate cache on review creation
+		reviews_dict = get_queried_reviews(self.website_item)
+		set_reviews_in_cache(self.website_item, reviews_dict)
+
+	def after_delete(self):
+		# regenerate cache on review deletion
+		reviews_dict = get_queried_reviews(self.website_item)
+		set_reviews_in_cache(self.website_item, reviews_dict)
+
+
+@frappe.whitelist()
+def get_item_reviews(web_item, start=0, end=10, data=None):
+	"Get Website Item Review Data."
+	start, end = cint(start), cint(end)
+	settings = get_shopping_cart_settings()
+
+	# Get cached reviews for first page (start=0)
+	# avoid cache when page is different
+	from_cache = not bool(start)
+
+	if not data:
+		data = frappe._dict()
+
+	if settings and settings.get("enable_reviews"):
+		reviews_cache = frappe.cache().hget("item_reviews", web_item)
+		if from_cache and reviews_cache:
+			data = reviews_cache
+		else:
+			data = get_queried_reviews(web_item, start, end, data)
+			if from_cache:
+				set_reviews_in_cache(web_item, data)
+
+	return data
+
+def get_queried_reviews(web_item, start=0, end=10, data=None):
+	"""
+		Query Website Item wise reviews and cache if needed.
+		Cache stores only first page of reviews i.e. 10 reviews maximum.
+		Returns:
+			dict: Containing reviews, average ratings, % of reviews per rating and total reviews.
+	"""
+	if not data:
+		data = frappe._dict()
+
+	data.reviews = frappe.db.get_all(
+		"Item Review",
+		filters={"website_item": web_item},
+		fields=["*"],
+		limit_start=start,
+		limit_page_length=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
+
+def set_reviews_in_cache(web_item, reviews_dict):
+	frappe.cache().hset("item_reviews", web_item, reviews_dict)
+
+@frappe.whitelist()
+def add_item_review(web_item, title, rating, comment=None):
+	""" Add an Item Review by a user if non-existent. """
+	if frappe.session.user == "Guest":
+		# guest user should not reach here ideally in the case they do via an API, throw error
+		frappe.throw(_("You are not verified to write a review yet."), exc=UnverifiedReviewer)
+
+	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(silent=False):
+	"""
+		silent: Return customer if exists else return nothing. Dont throw error.
+	"""
+	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)
+	elif silent:
+		return None
+	else:
+		# should not reach here unless via an API
+		frappe.throw(_("You are not a verified customer yet. Please contact us to proceed."),
+			exc=UnverifiedReviewer)
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..8a4befc
--- /dev/null
+++ b/erpnext/e_commerce/doctype/item_review/test_item_review.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+import unittest
+
+import frappe
+from frappe.core.doctype.user_permission.test_user_permission import create_user
+
+from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
+	setup_e_commerce_settings,
+)
+from erpnext.e_commerce.doctype.item_review.item_review import (
+	UnverifiedReviewer,
+	add_item_review,
+	get_item_reviews,
+)
+from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
+from erpnext.e_commerce.shopping_cart.cart import get_party
+from erpnext.stock.doctype.item.test_item import make_item
+
+
+class TestItemReview(unittest.TestCase):
+	def setUp(self):
+		item = make_item("Test Mobile Phone")
+		if not frappe.db.exists("Website Item", {"item_code": "Test Mobile Phone"}):
+			make_website_item(item, save=True)
+
+		setup_e_commerce_settings({"enable_reviews": 1})
+		frappe.local.shopping_cart_settings = None
+
+	def tearDown(self):
+		frappe.get_cached_doc("Website Item", {"item_code": "Test Mobile Phone"}).delete()
+		setup_e_commerce_settings({"enable_reviews": 0})
+
+	def test_add_and_get_item_reviews_from_customer(self):
+		"Add / Get Reviews from a User that is a valid customer (has added to cart or purchased in the past)"
+		# create user
+		web_item = frappe.db.get_value("Website Item", {"item_code": "Test Mobile Phone"})
+		test_user = create_user("test_reviewer@example.com", "Customer")
+		frappe.set_user(test_user.name)
+
+		# create customer and contact against user
+		customer = get_party()
+
+		# post review on "Test Mobile Phone"
+		try:
+			add_item_review(web_item, "Great Product", 3, "Would recommend this product")
+			review_name = frappe.db.get_value("Item Review", {"website_item": web_item})
+		except Exception:
+			self.fail(f"Error while publishing review for {web_item}")
+
+		review_data = get_item_reviews(web_item, 0, 10)
+
+		self.assertEqual(len(review_data.reviews), 1)
+		self.assertEqual(review_data.average_rating, 3)
+		self.assertEqual(review_data.reviews_per_rating[2], 100)
+
+		# tear down
+		frappe.set_user("Administrator")
+		frappe.delete_doc("Item Review", review_name)
+		customer.delete()
+
+	def test_add_item_review_from_non_customer(self):
+		"Check if logged in user (who is not a customer yet) is blocked from posting reviews."
+		web_item = frappe.db.get_value("Website Item", {"item_code": "Test Mobile Phone"})
+		test_user = create_user("test_reviewer@example.com", "Customer")
+		frappe.set_user(test_user.name)
+
+		with self.assertRaises(UnverifiedReviewer):
+			add_item_review(web_item, "Great Product", 3, "Would recommend this product")
+
+		# tear down
+		frappe.set_user("Administrator")
+
+	def test_add_item_reviews_from_guest_user(self):
+		"Check if Guest user is blocked from posting reviews."
+		web_item = frappe.db.get_value("Website Item", {"item_code": "Test Mobile Phone"})
+		frappe.set_user("Guest")
+
+		with self.assertRaises(UnverifiedReviewer):
+			add_item_review(web_item, "Great Product", 3, "Would recommend this product")
+
+		# tear down
+		frappe.set_user("Administrator")
diff --git a/erpnext/portal/doctype/products_settings/__init__.py b/erpnext/e_commerce/doctype/recommended_items/__init__.py
similarity index 100%
copy from erpnext/portal/doctype/products_settings/__init__.py
copy to erpnext/e_commerce/doctype/recommended_items/__init__.py
diff --git a/erpnext/e_commerce/doctype/recommended_items/recommended_items.json b/erpnext/e_commerce/doctype/recommended_items/recommended_items.json
new file mode 100644
index 0000000..06ac3dc
--- /dev/null
+++ b/erpnext/e_commerce/doctype/recommended_items/recommended_items.json
@@ -0,0 +1,87 @@
+{
+ "actions": [],
+ "creation": "2021-07-12 20:52:12.503470",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "website_item",
+  "website_item_name",
+  "column_break_2",
+  "item_code",
+  "more_information_section",
+  "route",
+  "column_break_6",
+  "website_item_image",
+  "website_item_thumbnail"
+ ],
+ "fields": [
+  {
+   "fieldname": "website_item",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Website Item",
+   "options": "Website Item"
+  },
+  {
+   "fetch_from": "website_item.web_item_name",
+   "fieldname": "website_item_name",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Website Item Name",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_2",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "more_information_section",
+   "fieldtype": "Section Break",
+   "label": "More Information"
+  },
+  {
+   "fetch_from": "website_item.route",
+   "fieldname": "route",
+   "fieldtype": "Small Text",
+   "label": "Route",
+   "read_only": 1
+  },
+  {
+   "fetch_from": "website_item.image",
+   "fieldname": "website_item_image",
+   "fieldtype": "Attach",
+   "label": "Website Item Image",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_6",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fetch_from": "website_item.thumbnail",
+   "fieldname": "website_item_thumbnail",
+   "fieldtype": "Data",
+   "label": "Website Item Thumbnail",
+   "read_only": 1
+  },
+  {
+   "fetch_from": "website_item.item_code",
+   "fieldname": "item_code",
+   "fieldtype": "Data",
+   "label": "Item Code"
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-07-13 21:02:19.031652",
+ "modified_by": "Administrator",
+ "module": "E-commerce",
+ "name": "Recommended 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/recommended_items/recommended_items.py b/erpnext/e_commerce/doctype/recommended_items/recommended_items.py
new file mode 100644
index 0000000..16b6e52
--- /dev/null
+++ b/erpnext/e_commerce/doctype/recommended_items/recommended_items.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class RecommendedItems(Document):
+	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..b39e4df
--- /dev/null
+++ b/erpnext/e_commerce/doctype/website_item/test_website_item.py
@@ -0,0 +1,538 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import unittest
+
+import frappe
+
+from erpnext.controllers.item_variant import create_variant
+from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
+	get_shopping_cart_settings,
+)
+from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
+	setup_e_commerce_settings,
+)
+from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
+from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
+from erpnext.stock.doctype.item.item import DataValidationError
+from erpnext.stock.doctype.item.test_item import make_item
+
+WEBITEM_DESK_TESTS = ("test_website_item_desk_item_sync", "test_publish_variant_and_template")
+WEBITEM_PRICE_TESTS = ('test_website_item_price_for_logged_in_user', 'test_website_item_price_for_guest_user')
+
+class TestWebsiteItem(unittest.TestCase):
+	@classmethod
+	def setUpClass(cls):
+		setup_e_commerce_settings({
+			"company": "_Test Company",
+			"enabled": 1,
+			"default_customer_group": "_Test Customer Group",
+			"price_list": "_Test Price List India"
+		})
+
+	@classmethod
+	def tearDownClass(cls):
+		frappe.db.rollback()
+
+	def setUp(self):
+		if self._testMethodName in WEBITEM_DESK_TESTS:
+			make_item("Test Web Item", {
+				"has_variant": 1,
+				"variant_based_on": "Item Attribute",
+				"attributes": [
+					{
+						"attribute": "Test Size"
+					}
+				]
+			})
+		elif self._testMethodName in WEBITEM_PRICE_TESTS:
+			create_user_and_customer_if_not_exists("test_contact_customer@example.com", "_Test Contact For _Test Customer")
+			create_regular_web_item()
+			make_web_item_price(item_code="Test Mobile Phone")
+
+			# Note: When testing web item pricing rule logged-in user pricing rule must differ from guest pricing rule or test will falsely pass.
+			#	  This is because make_web_pricing_rule creates a pricing rule "selling": 1, without specifying "applicable_for". Therefor,
+			#	  when testing for logged-in user the test will get the previous pricing rule because "selling" is still true.
+			#
+			#     I've attempted to mitigate this by setting applicable_for=Customer, and customer=Guest however, this only results in PermissionError failing the test.
+			make_web_pricing_rule(
+				title="Test Pricing Rule for Test Mobile Phone",
+				item_code="Test Mobile Phone",
+				selling=1)
+			make_web_pricing_rule(
+				title="Test Pricing Rule for Test Mobile Phone (Customer)",
+				item_code="Test Mobile Phone",
+				selling=1,
+				discount_percentage="25",
+				applicable_for="Customer",
+				customer="_Test Customer")
+
+	def test_index_creation(self):
+		"Check if index is getting created in db."
+		from erpnext.e_commerce.doctype.website_item.website_item import on_doctype_update
+		on_doctype_update()
+
+		indices = frappe.db.sql("show index from `tabWebsite Item`", as_dict=1)
+		expected_columns = {"route", "item_group", "brand"}
+		for index in indices:
+			expected_columns.discard(index.get("Column_name"))
+
+		if expected_columns:
+			self.fail(f"Expected db index on these columns: {', '.join(expected_columns)}")
+
+	def test_website_item_desk_item_sync(self):
+		"Check creation/updation/deletion of Website Item and its impact on Item master."
+		web_item = None
+		item = make_item("Test Web Item") # will return item if exists
+		try:
+			web_item = make_website_item(item, save=False)
+			web_item.save()
+		except Exception:
+			self.fail(f"Error while creating website item for {item}")
+
+		# check if website item was created
+		self.assertTrue(bool(web_item))
+		self.assertTrue(bool(web_item.route))
+
+		item.reload()
+		self.assertEqual(web_item.published, 1)
+		self.assertEqual(item.published_in_website, 1) # check if item was back updated
+		self.assertEqual(web_item.item_group, item.item_group)
+
+		# check if changing item data changes it in website item
+		item.item_name = "Test Web Item 1"
+		item.stock_uom = "Unit"
+		item.save()
+		web_item.reload()
+		self.assertEqual(web_item.item_name, item.item_name)
+		self.assertEqual(web_item.stock_uom, item.stock_uom)
+
+		# check if disabling item unpublished website item
+		item.disabled = 1
+		item.save()
+		web_item.reload()
+		self.assertEqual(web_item.published, 0)
+
+		# check if website item deletion, unpublishes desk item
+		web_item.delete()
+		item.reload()
+		self.assertEqual(item.published_in_website, 0)
+
+		item.delete()
+
+	def test_publish_variant_and_template(self):
+		"Check if template is published on publishing variant."
+		# template "Test Web Item" created on setUp
+		variant = create_variant("Test Web Item", {"Test Size": "Large"})
+		variant.save()
+
+		# check if template is not published
+		self.assertIsNone(frappe.db.exists("Website Item", {"item_code": variant.variant_of}))
+
+		variant_web_item = make_website_item(variant, save=False)
+		variant_web_item.save()
+
+		# check if template is published
+		try:
+			template_web_item = frappe.get_doc("Website Item", {"item_code": variant.variant_of})
+		except frappe.DoesNotExistError:
+			self.fail(f"Template of {variant.item_code}, {variant.variant_of} not published")
+
+		# teardown
+		variant_web_item.delete()
+		template_web_item.delete()
+		variant.delete()
+
+	def test_impact_on_merging_items(self):
+		"Check if merging items is blocked if old and new items both have website items"
+		first_item = make_item("Test First Item")
+		second_item = make_item("Test Second Item")
+
+		first_web_item = make_website_item(first_item, save=False)
+		first_web_item.save()
+		second_web_item = make_website_item(second_item, save=False)
+		second_web_item.save()
+
+		with self.assertRaises(DataValidationError):
+			frappe.rename_doc("Item", "Test First Item", "Test Second Item", merge=True)
+
+		# tear down
+		second_web_item.delete()
+		first_web_item.delete()
+		second_item.delete()
+		first_item.delete()
+
+	# Website Item Portal Tests Begin
+
+	def test_website_item_breadcrumbs(self):
+		"Check if breadcrumbs include homepage, product listing navigation page, parent item group(s) and item group."
+		from erpnext.setup.doctype.item_group.item_group import get_parent_item_groups
+
+		item_code = "Test Breadcrumb Item"
+		item = make_item(item_code, {
+			"item_group": "_Test Item Group B - 1",
+		})
+
+		if not frappe.db.exists("Website Item", {"item_code": item_code}):
+			web_item = make_website_item(item, save=False)
+			web_item.save()
+		else:
+			web_item = frappe.get_cached_doc("Website Item", {"item_code": item_code})
+
+		frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1)
+		frappe.db.set_value("Item Group", "_Test Item Group B", "show_in_website", 1)
+
+		breadcrumbs = get_parent_item_groups(item.item_group)
+
+		self.assertEqual(breadcrumbs[0]["name"], "Home")
+		self.assertEqual(breadcrumbs[1]["name"], "Shop by Category")
+		self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group
+		self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1")
+
+		# tear down
+		web_item.delete()
+		item.delete()
+
+	def test_website_item_price_for_logged_in_user(self):
+		"Check if price details are fetched correctly while logged in."
+		item_code = "Test Mobile Phone"
+
+		# show price in e commerce settings
+		setup_e_commerce_settings({"show_price": 1})
+
+		# price and pricing rule added via setUp
+
+		# login as customer with pricing rule
+		frappe.set_user("test_contact_customer@example.com")
+
+		# check if price and slashed price is fetched correctly
+		frappe.local.shopping_cart_settings = None
+		data = get_product_info_for_website(item_code, skip_quotation_creation=True)
+		self.assertTrue(bool(data.product_info["price"]))
+
+		price_object = data.product_info["price"]
+		self.assertEqual(price_object.get("discount_percent"), 25)
+		self.assertEqual(price_object.get("price_list_rate"), 750)
+		self.assertEqual(price_object.get("formatted_mrp"), "₹ 1,000.00")
+		self.assertEqual(price_object.get("formatted_price"), "₹ 750.00")
+		self.assertEqual(price_object.get("formatted_discount_percent"), "25%")
+
+		# switch to admin and disable show price
+		frappe.set_user("Administrator")
+		setup_e_commerce_settings({"show_price": 0})
+
+		# price should not be fetched for logged in user.
+		frappe.set_user("test_contact_customer@example.com")
+		frappe.local.shopping_cart_settings = None
+		data = get_product_info_for_website(item_code, skip_quotation_creation=True)
+		self.assertFalse(bool(data.product_info["price"]))
+
+		# tear down
+		frappe.set_user("Administrator")
+
+	def test_website_item_price_for_guest_user(self):
+		"Check if price details are fetched correctly for guest user."
+		item_code = "Test Mobile Phone"
+
+		# show price for guest user in e commerce settings
+		setup_e_commerce_settings({
+			"show_price": 1,
+			"hide_price_for_guest": 0
+		})
+
+		# price and pricing rule added via setUp
+
+		# switch to guest user
+		frappe.set_user("Guest")
+
+		# price should be fetched
+		frappe.local.shopping_cart_settings = None
+		data = get_product_info_for_website(item_code, skip_quotation_creation=True)
+		self.assertTrue(bool(data.product_info["price"]))
+
+		price_object = data.product_info["price"]
+		self.assertEqual(price_object.get("discount_percent"), 10)
+		self.assertEqual(price_object.get("price_list_rate"), 900)
+
+		# hide price for guest user
+		frappe.set_user("Administrator")
+		setup_e_commerce_settings({"hide_price_for_guest": 1})
+		frappe.set_user("Guest")
+
+		# price should not be fetched
+		frappe.local.shopping_cart_settings = None
+		data = get_product_info_for_website(item_code, skip_quotation_creation=True)
+		self.assertFalse(bool(data.product_info["price"]))
+
+		# tear down
+		frappe.set_user("Administrator")
+
+	def test_website_item_stock_when_out_of_stock(self):
+		"""
+			Check if stock details are fetched correctly for empty inventory when:
+			1) Showing stock availability enabled:
+				- Warehouse unset
+				- Warehouse set
+			2) Showing stock availability disabled
+		"""
+		item_code = "Test Mobile Phone"
+		create_regular_web_item()
+		setup_e_commerce_settings({"show_stock_availability": 1})
+
+		frappe.local.shopping_cart_settings = None
+		data = get_product_info_for_website(item_code, skip_quotation_creation=True)
+
+		# check if stock details are fetched and item not in stock without warehouse set
+		self.assertFalse(bool(data.product_info["in_stock"]))
+		self.assertFalse(bool(data.product_info["stock_qty"]))
+
+		# set warehouse
+		frappe.db.set_value("Website Item", {"item_code": item_code}, "website_warehouse", "_Test Warehouse - _TC")
+
+		# check if stock details are fetched and item not in stock with warehouse set
+		data = get_product_info_for_website(item_code, skip_quotation_creation=True)
+		self.assertFalse(bool(data.product_info["in_stock"]))
+		self.assertEqual(data.product_info["stock_qty"][0][0], 0)
+
+		# disable show stock availability
+		setup_e_commerce_settings({"show_stock_availability": 0})
+		frappe.local.shopping_cart_settings = None
+		data = get_product_info_for_website(item_code, skip_quotation_creation=True)
+
+		# check if stock detail attributes are not fetched if stock availability is hidden
+		self.assertIsNone(data.product_info.get("in_stock"))
+		self.assertIsNone(data.product_info.get("stock_qty"))
+		self.assertIsNone(data.product_info.get("show_stock_qty"))
+
+		# tear down
+		frappe.get_cached_doc("Website Item", {"item_code": "Test Mobile Phone"}).delete()
+
+	def test_website_item_stock_when_in_stock(self):
+		"""
+			Check if stock details are fetched correctly for available inventory when:
+			1) Showing stock availability enabled:
+				- Warehouse set
+				- Warehouse unset
+			2) Showing stock availability disabled
+		"""
+		from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+
+		item_code = "Test Mobile Phone"
+		create_regular_web_item()
+		setup_e_commerce_settings({"show_stock_availability": 1})
+		frappe.local.shopping_cart_settings = None
+
+		# set warehouse
+		frappe.db.set_value("Website Item", {"item_code": item_code}, "website_warehouse", "_Test Warehouse - _TC")
+
+		# stock up item
+		stock_entry = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=2, rate=100)
+
+		# check if stock details are fetched and item is in stock with warehouse set
+		data = get_product_info_for_website(item_code, skip_quotation_creation=True)
+		self.assertTrue(bool(data.product_info["in_stock"]))
+		self.assertEqual(data.product_info["stock_qty"][0][0], 2)
+
+		# unset warehouse
+		frappe.db.set_value("Website Item", {"item_code": item_code}, "website_warehouse", "")
+
+		# check if stock details are fetched and item not in stock without warehouse set
+		# (even though it has stock in some warehouse)
+		data = get_product_info_for_website(item_code, skip_quotation_creation=True)
+		self.assertFalse(bool(data.product_info["in_stock"]))
+		self.assertFalse(bool(data.product_info["stock_qty"]))
+
+		# disable show stock availability
+		setup_e_commerce_settings({"show_stock_availability": 0})
+		frappe.local.shopping_cart_settings = None
+		data = get_product_info_for_website(item_code, skip_quotation_creation=True)
+
+		# check if stock detail attributes are not fetched if stock availability is hidden
+		self.assertIsNone(data.product_info.get("in_stock"))
+		self.assertIsNone(data.product_info.get("stock_qty"))
+		self.assertIsNone(data.product_info.get("show_stock_qty"))
+
+		# tear down
+		stock_entry.cancel()
+		frappe.get_cached_doc("Website Item", {"item_code": "Test Mobile Phone"}).delete()
+
+	def test_recommended_item(self):
+		"Check if added recommended items are fetched correctly."
+		item_code = "Test Mobile Phone"
+		web_item = create_regular_web_item(item_code)
+
+		setup_e_commerce_settings({
+			"enable_recommendations": 1,
+			"show_price": 1
+		})
+
+		# create recommended web item and price for it
+		recommended_web_item = create_regular_web_item("Test Mobile Phone 1")
+		make_web_item_price(item_code="Test Mobile Phone 1")
+
+		# add recommended item to first web item
+		web_item.append("recommended_items", {"website_item": recommended_web_item.name})
+		web_item.save()
+
+		frappe.local.shopping_cart_settings = None
+		e_commerce_settings = get_shopping_cart_settings()
+		recommended_items = web_item.get_recommended_items(e_commerce_settings)
+
+		# test results if show price is enabled
+		self.assertEqual(len(recommended_items), 1)
+		recomm_item = recommended_items[0]
+		self.assertEqual(recomm_item.get("website_item_name"), "Test Mobile Phone 1")
+		self.assertTrue(bool(recomm_item.get("price_info"))) # price fetched
+
+		price_info = recomm_item.get("price_info")
+		self.assertEqual(price_info.get("price_list_rate"), 1000)
+		self.assertEqual(price_info.get("formatted_price"), "₹ 1,000.00")
+
+		# test results if show price is disabled
+		setup_e_commerce_settings({"show_price": 0})
+
+		frappe.local.shopping_cart_settings = None
+		e_commerce_settings = get_shopping_cart_settings()
+		recommended_items = web_item.get_recommended_items(e_commerce_settings)
+
+		self.assertEqual(len(recommended_items), 1)
+		self.assertFalse(bool(recommended_items[0].get("price_info"))) # price not fetched
+
+		# tear down
+		web_item.delete()
+		recommended_web_item.delete()
+		frappe.get_cached_doc("Item", "Test Mobile Phone 1").delete()
+
+	def test_recommended_item_for_guest_user(self):
+		"Check if added recommended items are fetched correctly for guest user."
+		item_code = "Test Mobile Phone"
+		web_item = create_regular_web_item(item_code)
+
+		# price visible to guests
+		setup_e_commerce_settings({
+			"enable_recommendations": 1,
+			"show_price": 1,
+			"hide_price_for_guest": 0
+		})
+
+		# create recommended web item and price for it
+		recommended_web_item = create_regular_web_item("Test Mobile Phone 1")
+		make_web_item_price(item_code="Test Mobile Phone 1")
+
+		# add recommended item to first web item
+		web_item.append("recommended_items", {"website_item": recommended_web_item.name})
+		web_item.save()
+
+		frappe.set_user("Guest")
+
+		frappe.local.shopping_cart_settings = None
+		e_commerce_settings = get_shopping_cart_settings()
+		recommended_items = web_item.get_recommended_items(e_commerce_settings)
+
+		# test results if show price is enabled
+		self.assertEqual(len(recommended_items), 1)
+		self.assertTrue(bool(recommended_items[0].get("price_info"))) # price fetched
+
+		# price hidden from guests
+		frappe.set_user("Administrator")
+		setup_e_commerce_settings({"hide_price_for_guest": 1})
+		frappe.set_user("Guest")
+
+		frappe.local.shopping_cart_settings = None
+		e_commerce_settings = get_shopping_cart_settings()
+		recommended_items = web_item.get_recommended_items(e_commerce_settings)
+
+		# test results if show price is enabled
+		self.assertEqual(len(recommended_items), 1)
+		self.assertFalse(bool(recommended_items[0].get("price_info"))) # price fetched
+
+		# tear down
+		frappe.set_user("Administrator")
+		web_item.delete()
+		recommended_web_item.delete()
+		frappe.get_cached_doc("Item", "Test Mobile Phone 1").delete()
+
+def create_regular_web_item(item_code=None, item_args=None, web_args=None):
+	"Create Regular Item and Website Item."
+	item_code = item_code or "Test Mobile Phone"
+	item = make_item(item_code, properties=item_args)
+
+	if not frappe.db.exists("Website Item", {"item_code": item_code}):
+		web_item = make_website_item(item, save=False)
+		if web_args:
+			web_item.update(web_args)
+		web_item.save()
+	else:
+		web_item = frappe.get_cached_doc("Website Item", {"item_code": item_code})
+
+	return web_item
+
+def make_web_item_price(**kwargs):
+	item_code = kwargs.get("item_code")
+	if not item_code:
+		return
+
+	if not frappe.db.exists("Item Price", {"item_code": item_code}):
+		item_price = frappe.get_doc({
+			"doctype": "Item Price",
+			"item_code": item_code,
+			"price_list": kwargs.get("price_list") or "_Test Price List India",
+			"price_list_rate": kwargs.get("price_list_rate") or 1000
+		})
+		item_price.insert()
+	else:
+		item_price = frappe.get_cached_doc("Item Price", {"item_code": item_code})
+
+	return item_price
+
+def make_web_pricing_rule(**kwargs):
+	title = kwargs.get("title")
+	if not title:
+		return
+
+	if not frappe.db.exists("Pricing Rule", title):
+		pricing_rule = frappe.get_doc({
+			"doctype": "Pricing Rule",
+			"title": title,
+			"apply_on": kwargs.get("apply_on") or "Item Code",
+			"items": [{
+				"item_code": kwargs.get("item_code")
+			}],
+			"selling": kwargs.get("selling") or 0,
+			"buying": kwargs.get("buying") or 0,
+			"rate_or_discount": kwargs.get("rate_or_discount") or "Discount Percentage",
+			"discount_percentage": kwargs.get("discount_percentage") or 10,
+			"company": kwargs.get("company") or "_Test Company",
+			"currency": kwargs.get("currency") or "INR",
+			"for_price_list": kwargs.get("price_list") or "_Test Price List India",
+			"applicable_for": kwargs.get("applicable_for") or "",
+			"customer": kwargs.get("customer") or "",
+		})
+		pricing_rule.insert()
+	else:
+		pricing_rule = frappe.get_doc("Pricing Rule", {"title": title})
+
+	return pricing_rule
+
+
+def create_user_and_customer_if_not_exists(email, first_name = None):
+	if frappe.db.exists("User", email):
+		return
+
+	frappe.get_doc({
+		"doctype": "User",
+		"user_type": "Website User",
+		"email": email,
+		"send_welcome_email": 0,
+		"first_name": first_name or email.split("@")[0]
+	}).insert(ignore_permissions=True)
+
+	contact = frappe.get_last_doc("Contact", filters={"email_id": email})
+	link = contact.append('links', {})
+	link.link_doctype = "Customer"
+	link.link_name = "_Test Customer"
+	link.link_title = "_Test Customer"
+	contact.save()
+
+test_dependencies = ["Price List", "Item Price", "Customer", "Contact", "Item"]
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..245042a
--- /dev/null
+++ b/erpnext/e_commerce/doctype/website_item/website_item.json
@@ -0,0 +1,415 @@
+{
+ "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",
+  "column_break_11",
+  "description",
+  "brand",
+  "image",
+  "display_section",
+  "website_image",
+  "website_image_alt",
+  "column_break_13",
+  "slideshow",
+  "thumbnail",
+  "stock_information_section",
+  "website_warehouse",
+  "column_break_24",
+  "on_backorder",
+  "section_break_17",
+  "short_description",
+  "web_long_description",
+  "column_break_27",
+  "website_specifications",
+  "copy_from_item_group",
+  "display_additional_information_section",
+  "show_tabbed_section",
+  "tabs",
+  "recommended_items_section",
+  "recommended_items",
+  "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"
+  },
+  {
+   "fieldname": "column_break_11",
+   "fieldtype": "Column Break"
+  },
+  {
+   "description": "Short Description for List View",
+   "fieldname": "short_description",
+   "fieldtype": "Small Text",
+   "label": "Short Website Description"
+  },
+  {
+   "collapsible": 1,
+   "fieldname": "recommended_items_section",
+   "fieldtype": "Section Break",
+   "label": "Recommended Items"
+  },
+  {
+   "fieldname": "recommended_items",
+   "fieldtype": "Table",
+   "label": "Recommended/Similar Items",
+   "options": "Recommended Items"
+  },
+  {
+   "fieldname": "stock_information_section",
+   "fieldtype": "Section Break",
+   "label": "Stock Information"
+  },
+  {
+   "fieldname": "column_break_24",
+   "fieldtype": "Column Break"
+  },
+  {
+   "default": "0",
+   "description": "Indicate that Item is available on backorder and not usually pre-stocked",
+   "fieldname": "on_backorder",
+   "fieldtype": "Check",
+   "label": "On Backorder"
+  }
+ ],
+ "has_web_view": 1,
+ "image_field": "image",
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-09-02 13:08:41.942726",
+ "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": "web_item_name, item_code, item_group",
+ "show_name_in_global_search": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "web_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..62f7f49
--- /dev/null
+++ b/erpnext/e_commerce/doctype/website_item/website_item.py
@@ -0,0 +1,441 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import json
+
+import frappe
+from frappe import _
+from frappe.utils import cint, cstr, flt, random_string
+from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow
+from frappe.website.website_generator import WebsiteGenerator
+
+from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
+from erpnext.e_commerce.redisearch_utils import (
+	delete_item_from_index,
+	insert_item_to_index,
+	update_index_for_item,
+)
+from erpnext.e_commerce.shopping_cart.cart import _set_price_list
+from erpnext.setup.doctype.item_group.item_group import (
+	get_parent_item_groups,
+	invalidate_cache_for,
+)
+from erpnext.utilities.product import get_price
+
+
+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 autoname(self):
+		# use naming series to accomodate items with same name (different item code)
+		from frappe.model.naming import make_autoname
+
+		from erpnext.setup.doctype.naming_series.naming_series import get_default_naming_series
+
+		naming_series = get_default_naming_series("Website Item")
+		if not self.name and naming_series:
+			self.name = make_autoname(naming_series, doc=self)
+
+	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"):
+			wig = frappe.qb.DocType("Website Item Group")
+			query = (
+				frappe.qb.from_(wig)
+				.select(wig.item_group)
+				.where(
+					(wig.parentfield == "website_item_groups")
+					& (wig.parenttype == "Website Item")
+					& (wig.parent == self.name)
+				)
+			)
+			result = query.run(as_list=True)
+
+			self.old_website_item_groups = [x[0] for x in result]
+
+	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_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):
+		"""Publish Template Item if Variant is published."""
+		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):
+		"""Make a thumbnail of `website_image`"""
+		if frappe.flags.in_import or frappe.flags.in_migrate:
+			return
+
+		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.body_class = "product-page"
+
+		context.parents = get_parent_item_groups(self.item_group, from_item=True) # breadcumbs
+		self.attributes = frappe.get_all(
+			"Item Variant Attribute",
+			fields=["attribute", "attribute_value"],
+			filters={"parent": self.item_code}
+		)
+
+		if self.slideshow:
+			context.update(get_slideshow(self))
+
+		self.set_metatags(context)
+		self.set_shopping_cart_data(context)
+
+		settings = context.shopping_cart.cart_settings
+
+		self.get_product_details_section(context)
+
+		if settings.get("enable_reviews"):
+			reviews_data = get_item_reviews(self.name)
+			context.update(reviews_data)
+			context.reviews = context.reviews[:4]
+
+		context.wished = False
+		if frappe.db.exists("Wishlist Item", {"item_code": self.item_code, "parent": frappe.session.user}):
+			context.wished = True
+
+		context.user_is_customer = check_if_user_is_customer()
+
+		context.recommended_items = None
+		if settings and settings.enable_recommendations:
+			context.recommended_items = self.get_recommended_items(settings)
+
+		return context
+
+	def set_selected_attributes(self, variants, context, attribute_values_available):
+		for variant in variants:
+			variant.attributes = frappe.get_all(
+				"Item Variant Attribute",
+				filters={"parent": variant.name},
+				fields=["attribute", "attribute_value as value"])
+
+			# make an attribute-value map for easier access in templates
+			variant.attribute_map = frappe._dict(
+				{attr.attribute : attr.value for attr in variant.attributes}
+			)
+
+			for attr in variant.attributes:
+				values = attribute_values_available.setdefault(attr.attribute, [])
+				if attr.value not in values:
+					values.append(attr.value)
+
+				if variant.name == context.variant.name:
+					context.selected_attributes[attr.attribute] = attr.value
+
+	def set_attribute_values(self, attributes, context, attribute_values_available):
+		for attr in 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)
+
+	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.web_item_name or 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 get_recommended_items(self, settings):
+		ri = frappe.qb.DocType("Recommended Items")
+		wi = frappe.qb.DocType("Website Item")
+
+		query = (
+			frappe.qb.from_(ri)
+			.join(wi).on(ri.item_code == wi.item_code)
+			.select(
+				ri.item_code, ri.route,
+				ri.website_item_name,
+				ri.website_item_thumbnail
+			).where(
+				(ri.parent == self.name)
+				& (wi.published == 1)
+			).orderby(ri.idx)
+		)
+		items = query.run(as_dict=True)
+
+		if settings.show_price:
+			is_guest = frappe.session.user == "Guest"
+			# Show Price if logged in.
+			# If not logged in and price is hidden for guest, skip price fetch.
+			if is_guest and settings.hide_price_for_guest:
+				return items
+
+			selling_price_list = _set_price_list(settings, None)
+			for item in items:
+				item.price_info = get_price(
+					item.item_code,
+					selling_price_list,
+					settings.default_customer_group,
+					settings.company
+				)
+
+		return items
+
+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)
+
+def on_doctype_update():
+	# since route is a Text column, it needs a length for indexing
+	frappe.db.add_index("Website Item", ["route(500)"])
+
+	frappe.db.add_index("Website Item", ["item_group"])
+	frappe.db.add_index("Website Item", ["brand"])
+
+def check_if_user_is_customer(user=None):
+	from frappe.contacts.doctype.contact.contact import get_contact_name
+
+	if not user:
+		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
+
+	return True if customer else False
+
+@frappe.whitelist()
+def make_website_item(doc, save=True):
+	if not doc:
+		return
+
+	if isinstance(doc, str):
+		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]
\ 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..91148b8
--- /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
+
+# 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..d73c132
--- /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
+
+import frappe
+from frappe.model.document import Document
+
+
+class WebsiteOffer(Document):
+	pass
+
+@frappe.whitelist(allow_guest=True)
+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..504bb65
--- /dev/null
+++ b/erpnext/e_commerce/doctype/wishlist/test_wishlist.py
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+import unittest
+
+import frappe
+from frappe.core.doctype.user_permission.test_user_permission import create_user
+
+from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
+from erpnext.e_commerce.doctype.wishlist.wishlist import add_to_wishlist, remove_from_wishlist
+from erpnext.stock.doctype.item.test_item import make_item
+
+
+class TestWishlist(unittest.TestCase):
+	def setUp(self):
+		item = make_item("Test Phone Series X")
+		if not frappe.db.exists("Website Item", {"item_code": "Test Phone Series X"}):
+			make_website_item(item, save=True)
+
+		item = make_item("Test Phone Series Y")
+		if not frappe.db.exists("Website Item", {"item_code": "Test Phone Series Y"}):
+			make_website_item(item, save=True)
+
+	def tearDown(self):
+		frappe.get_cached_doc("Website Item", {"item_code": "Test Phone Series X"}).delete()
+		frappe.get_cached_doc("Website Item", {"item_code": "Test Phone Series Y"}).delete()
+		frappe.get_cached_doc("Item", "Test Phone Series X").delete()
+		frappe.get_cached_doc("Item", "Test Phone Series Y").delete()
+
+	def test_add_remove_items_in_wishlist(self):
+		"Check if items are added and removed from user's wishlist."
+		# add first item
+		add_to_wishlist("Test Phone Series X")
+
+		# check if wishlist was created and item was added
+		self.assertTrue(frappe.db.exists("Wishlist", {"user": frappe.session.user}))
+		self.assertTrue(frappe.db.exists("Wishlist Item", {"item_code": "Test Phone Series X", "parent": frappe.session.user}))
+
+		# add second item to wishlist
+		add_to_wishlist("Test Phone Series Y")
+		wishlist_length = frappe.db.get_value(
+			"Wishlist Item",
+			{"parent": frappe.session.user},
+			"count(*)"
+		)
+		self.assertEqual(wishlist_length, 2)
+
+		remove_from_wishlist("Test Phone Series X")
+		remove_from_wishlist("Test Phone Series Y")
+
+		wishlist_length = frappe.db.get_value(
+			"Wishlist Item",
+			{"parent": frappe.session.user},
+			"count(*)"
+		)
+		self.assertIsNone(frappe.db.exists("Wishlist Item", {"parent": frappe.session.user}))
+		self.assertEqual(wishlist_length, 0)
+
+		# tear down
+		frappe.get_doc("Wishlist", {"user": frappe.session.user}).delete()
+
+	def test_add_remove_in_wishlist_multiple_users(self):
+		"Check if items are added and removed from the correct user's wishlist."
+		test_user = create_user("test_reviewer@example.com", "Customer")
+		test_user_1 = create_user("test_reviewer_1@example.com", "Customer")
+
+		# add to wishlist for first user
+		frappe.set_user(test_user.name)
+		add_to_wishlist("Test Phone Series X")
+
+		# add to wishlist for second user
+		frappe.set_user(test_user_1.name)
+		add_to_wishlist("Test Phone Series X")
+
+		# check wishlist and its content for users
+		self.assertTrue(frappe.db.exists("Wishlist", {"user": test_user.name}))
+		self.assertTrue(frappe.db.exists("Wishlist Item",
+			{"item_code": "Test Phone Series X", "parent": test_user.name}))
+
+		self.assertTrue(frappe.db.exists("Wishlist", {"user": test_user_1.name}))
+		self.assertTrue(frappe.db.exists("Wishlist Item",
+			{"item_code": "Test Phone Series X", "parent": test_user_1.name}))
+
+		# remove item for second user
+		remove_from_wishlist("Test Phone Series X")
+
+		# make sure item was removed for second user and not first
+		self.assertFalse(frappe.db.exists("Wishlist Item",
+			{"item_code": "Test Phone Series X", "parent": test_user_1.name}))
+		self.assertTrue(frappe.db.exists("Wishlist Item",
+			{"item_code": "Test Phone Series X", "parent": test_user.name}))
+
+		# remove item for first user
+		frappe.set_user(test_user.name)
+		remove_from_wishlist("Test Phone Series X")
+		self.assertFalse(frappe.db.exists("Wishlist Item",
+			{"item_code": "Test Phone Series X", "parent": test_user.name}))
+
+		# tear down
+		frappe.set_user("Administrator")
+		frappe.get_doc("Wishlist", {"user": test_user.name}).delete()
+		frappe.get_doc("Wishlist", {"user": test_user_1.name}).delete()
\ No newline at end of file
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..922924e
--- /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 Item"
+  }
+ ],
+ "in_create": 1,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-07-08 13:11:21.693956",
+ "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..50e3d3a
--- /dev/null
+++ b/erpnext/e_commerce/doctype/wishlist/wishlist.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.model.document import Document
+
+
+class Wishlist(Document):
+	pass
+
+@frappe.whitelist()
+def add_to_wishlist(item_code):
+	"""Insert Item into wishlist."""
+
+	if frappe.db.exists("Wishlist Item", {"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", "web_item_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"),
+		"web_item_name": web_item_data.get("web_item_name"),
+		"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 Item", {"item_code": item_code, "parent": frappe.session.user}):
+		frappe.db.delete(
+			"Wishlist Item",
+			{
+				"item_code": item_code,
+				"parent": frappe.session.user
+			}
+		)
+		frappe.db.commit() # nosemgrep
+
+		wishlist_items = frappe.db.get_values(
+			"Wishlist Item",
+			filters={"parent": 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_item/__init__.py
similarity index 100%
copy from erpnext/portal/doctype/products_settings/__init__.py
copy to erpnext/e_commerce/doctype/wishlist_item/__init__.py
diff --git a/erpnext/e_commerce/doctype/wishlist_item/wishlist_item.json b/erpnext/e_commerce/doctype/wishlist_item/wishlist_item.json
new file mode 100644
index 0000000..c0414a7
--- /dev/null
+++ b/erpnext/e_commerce/doctype/wishlist_item/wishlist_item.json
@@ -0,0 +1,147 @@
+{
+ "actions": [],
+ "creation": "2021-03-10 19:03:00.662714",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "item_code",
+  "website_item",
+  "web_item_name",
+  "column_break_3",
+  "item_name",
+  "item_group",
+  "item_details_section",
+  "description",
+  "column_break_7",
+  "route",
+  "image",
+  "image_view",
+  "section_break_8",
+  "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",
+   "read_only": 1
+  },
+  {
+   "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",
+   "read_only": 1
+  },
+  {
+   "collapsible": 1,
+   "fieldname": "item_details_section",
+   "fieldtype": "Section Break",
+   "label": "Item Details",
+   "read_only": 1
+  },
+  {
+   "fetch_from": "item_code.description",
+   "fetch_if_empty": 1,
+   "fieldname": "description",
+   "fieldtype": "Text Editor",
+   "label": "Description",
+   "read_only": 1
+  },
+  {
+   "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",
+   "read_only": 1
+  },
+  {
+   "fieldname": "section_break_8",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fetch_from": "item_code.item_group",
+   "fetch_if_empty": 1,
+   "fieldname": "item_group",
+   "fieldtype": "Link",
+   "label": "Item Group",
+   "options": "Item Group",
+   "read_only": 1
+  },
+  {
+   "fetch_from": "website_item.route",
+   "fetch_if_empty": 1,
+   "fieldname": "route",
+   "fieldtype": "Small Text",
+   "label": "Route",
+   "read_only": 1
+  },
+  {
+   "fetch_from": "website_item.web_item_name",
+   "fetch_if_empty": 1,
+   "fieldname": "web_item_name",
+   "fieldtype": "Data",
+   "label": "Website Item Name",
+   "read_only": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-08-09 10:30:41.964802",
+ "modified_by": "Administrator",
+ "module": "E-commerce",
+ "name": "Wishlist Item",
+ "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_item/wishlist_item.py b/erpnext/e_commerce/doctype/wishlist_item/wishlist_item.py
new file mode 100644
index 0000000..75ebccb
--- /dev/null
+++ b/erpnext/e_commerce/doctype/wishlist_item/wishlist_item.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class WishlistItem(Document):
+	pass
diff --git a/erpnext/shopping_cart/search.py b/erpnext/e_commerce/legacy_search.py
similarity index 95%
rename from erpnext/shopping_cart/search.py
rename to erpnext/e_commerce/legacy_search.py
index 5d2de78..752c33e 100644
--- a/erpnext/shopping_cart/search.py
+++ b/erpnext/e_commerce/legacy_search.py
@@ -6,6 +6,7 @@
 from whoosh.qparser import FieldsPlugin, MultifieldParser, WildcardPlugin
 from whoosh.query import Prefix
 
+# TODO: Make obsolete
 INDEX_NAME = "products"
 
 class ProductSearch(FullTextSearch):
@@ -111,7 +112,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/e_commerce/product_data_engine/filters.py b/erpnext/e_commerce/product_data_engine/filters.py
new file mode 100644
index 0000000..c4a3cb9
--- /dev/null
+++ b/erpnext/e_commerce/product_data_engine/filters.py
@@ -0,0 +1,139 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+import frappe
+from frappe.utils import floor
+
+
+class ProductFiltersBuilder:
+	def __init__(self, item_group=None):
+		if not item_group:
+			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
+
+		fields, filter_data = [], []
+		filter_fields = [row.fieldname for row in self.doc.filter_fields] # fields in settings
+
+		# filter valid field filters i.e. those that exist in Item
+		item_meta = frappe.get_meta('Item', cached=True)
+		fields = [item_meta.get_field(field) for field in filter_fields if item_meta.has_field(field)]
+
+		for df in fields:
+			item_filters, item_or_filters = {}, []
+			link_doctype_values = self.get_filtered_link_doctype_records(df)
+
+			if df.fieldtype == "Link":
+				if self.item_group:
+					item_or_filters.extend([
+						["item_group", "=", self.item_group],
+						["Website Item Group", "item_group", "=", self.item_group] # consider website item groups
+					])
+
+				# Get link field values attached to published items
+				item_filters['published_in_website'] = 1
+				item_values = frappe.get_all(
+					"Item",
+					fields=[df.fieldname],
+					filters=item_filters,
+					or_filters=item_or_filters,
+					distinct="True",
+					pluck=df.fieldname
+				)
+
+				values = list(set(item_values) & link_doctype_values) # intersection of both
+			else:
+				# table multiselect
+				values = list(link_doctype_values)
+
+			# Remove None
+			if None in values:
+				values.remove(None)
+
+			if values:
+				filter_data.append([df, values])
+
+		return filter_data
+
+	def get_filtered_link_doctype_records(self, field):
+		"""
+			Get valid link doctype records depending on filters.
+			Apply enable/disable/show_in_website filter.
+			Returns:
+				set: A set containing valid record names
+		"""
+		link_doctype = field.get_link_doctype()
+		meta = frappe.get_meta(link_doctype, cached=True) if link_doctype else None
+		if meta:
+			filters = self.get_link_doctype_filters(meta)
+			link_doctype_values = set(d.name for d in frappe.get_all(link_doctype, filters))
+
+		return link_doctype_values if meta else set()
+
+	def get_link_doctype_filters(self, meta):
+		"Filters for Link Doctype eg. 'show_in_website'."
+		filters = {}
+		if not meta:
+			return 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
+
+		return filters
+
+	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:
+			return []
+
+		result = frappe.get_all(
+			"Item Variant Attribute",
+			filters={
+				"attribute": ["in", attributes],
+				"attribute_value": ["is", "set"]
+			},
+			fields=["attribute", "attribute_value"],
+			distinct=True
+		)
+
+		attribute_value_map = {}
+		for d in result:
+			attribute_value_map.setdefault(d.attribute, []).append(d.attribute_value)
+
+		out = []
+		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 max
+		min_discount, max_discount = discounts[0], discounts[1]
+		# [25, 60] rounded min max
+		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
+
+		min_range = (min_range + 10) if min_range != min_range_absolute else min_range # 30 (upper limit of 25.89 in range of 10)
+		max_range = (max_range + 10) if max_range != max_range_absolute else max_range # 60
+
+		for discount in range(min_range, (max_range + 1), 10):
+			label = f"{discount}% and below"
+			discount_filters.append([discount, label])
+
+		return discount_filters
diff --git a/erpnext/e_commerce/product_data_engine/query.py b/erpnext/e_commerce/product_data_engine/query.py
new file mode 100644
index 0000000..007bf8b
--- /dev/null
+++ b/erpnext/e_commerce/product_data_engine/query.py
@@ -0,0 +1,301 @@
+# Copyright (c) 2021, 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.doctype.item_review.item_review import get_customer
+from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
+from erpnext.utilities.product import get_non_stock_item_status
+
+
+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.or_filters = []
+		self.filters = [["published", "=", 1]]
+		self.fields = [
+			"web_item_name", "name", "item_name", "item_code", "website_image",
+			"variant_of", "has_variants", "item_group", "image", "web_long_description",
+			"short_description", "route", "website_warehouse", "ranking", "on_backorder"
+		]
+
+	def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None):
+		"""
+		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:
+			dict: Dict containing items, item count & discount range
+		"""
+		# track if discounts included in field filters
+		self.filter_with_discount = bool(fields.get("discount"))
+		result, discount_list, website_item_groups, cart_items, count = [], [], [], [], 0
+
+		website_item_groups = self.get_website_item_group_results(item_group, website_item_groups)
+
+		if fields:
+			self.build_fields_filters(fields)
+		if search_term:
+			self.build_search_filters(search_term)
+		if self.settings.hide_variants:
+			self.filters.append(["variant_of", "is", "not set"])
+
+		# query results
+		if attributes:
+			result, count = self.query_items_with_attributes(attributes, start)
+		else:
+			result, count = self.query_items(start=start)
+
+		result = self.combine_web_item_group_results(item_group, result, website_item_groups)
+
+		# sort combined results by ranking
+		result = sorted(result, key=lambda x: x.get("ranking"), reverse=True)
+
+		if self.settings.enabled:
+			cart_items = self.get_cart_items()
+
+		result, discount_list = self.add_display_details(result, discount_list, cart_items)
+
+		discounts = []
+		if discount_list:
+			discounts = [min(discount_list), max(discount_list)]
+
+		result = self.filter_results_by_discount(fields, result)
+
+		return {
+			"items": result,
+			"items_count": count,
+			"discounts": discounts
+		}
+
+	def query_items(self, start=0):
+		"""Build a query to fetch Website Items based on field filters."""
+		# MySQL does not support offset without limit,
+		# frappe does not accept two parameters for limit
+		# https://dev.mysql.com/doc/refman/8.0/en/select.html#id4651989
+		count_items = frappe.db.get_all(
+			"Website Item",
+			filters=self.filters,
+			or_filters=self.or_filters,
+			limit_page_length=184467440737095516,
+			limit_start=start, # get all items from this offset for total count ahead
+			order_by="ranking desc")
+		count = len(count_items)
+
+		# If discounts included, return all rows.
+		# Slice after filtering rows with discount (See `filter_results_by_discount`).
+		# Slicing before hand will miss discounted items on the 3rd or 4th page.
+		# Discounts are fetched on computing Pricing Rules so we cannot query them directly.
+		page_length = 184467440737095516 if self.filter_with_discount else self.page_length
+
+		items = frappe.db.get_all(
+			"Website Item",
+			fields=self.fields,
+			filters=self.filters,
+			or_filters=self.or_filters,
+			limit_page_length=page_length,
+			limit_start=start,
+			order_by="ranking desc")
+
+		return items, count
+
+	def query_items_with_attributes(self, attributes, start=0):
+		"""Build a query to fetch Website Items based on field & attribute filters."""
+		item_codes = []
+
+		for attribute, values in attributes.items():
+			if not isinstance(values, list):
+				values = [values]
+
+			# get items that have selected attribute & value
+			item_code_list = frappe.db.get_all(
+				"Item",
+				fields=["item_code"],
+				filters=[
+					["published_in_website", "=", 1],
+					["Item Variant Attribute", "attribute", "=", attribute],
+					["Item Variant Attribute", "attribute_value", "in", values]
+				])
+			item_codes.append({x.item_code for x in item_code_list})
+
+		if item_codes:
+			item_codes = list(set.intersection(*item_codes))
+			self.filters.append(["item_code", "in", item_codes])
+
+		items, count = self.query_items(start=start)
+
+		return items, count
+
+	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('Website 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 = {'item_code', 'item_name', 'web_long_description', 'item_group'}
+
+		# Get meta search fields
+		meta = frappe.get_meta("Website Item")
+		meta_fields = set(meta.get_search_fields())
+
+		# Join the meta fields and default fields set
+		search_fields = default_fields.union(meta_fields)
+		if frappe.db.count('Website Item', cache=True) > 50000:
+			search_fields.discard('web_long_description')
+
+		# Build or filters for query
+		search = '%{}%'.format(search_term)
+		for field in search_fields:
+			self.or_filters.append([field, "like", search])
+
+	def get_website_item_group_results(self, item_group, website_item_groups):
+		"""Get Web Items for Item Group Page via Website Item Groups."""
+		if item_group:
+			website_item_groups = frappe.db.get_all(
+				"Website Item",
+				fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
+				filters=[
+					["Website Item Group", "item_group", "=", item_group],
+					["published", "=", 1]
+				]
+			)
+		return website_item_groups
+
+	def add_display_details(self, result, discount_list, cart_items):
+		"""Add price and availability details in result."""
+		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']:
+				# update/mutate item and discount_list objects
+				self.get_price_discount_info(item, product_info['price'], discount_list)
+
+			if self.settings.show_stock_availability:
+				self.get_stock_availability(item)
+
+			item.in_cart = item.item_code in cart_items
+
+			item.wished = False
+			if frappe.db.exists("Wishlist Item", {"item_code": item.item_code, "parent": frappe.session.user}):
+				item.wished = True
+
+		return result, discount_list
+
+	def get_price_discount_info(self, item, price_object, discount_list):
+		"""Modify item object and add price details."""
+		fields = ["formatted_mrp", "formatted_price", "price_list_rate"]
+		for field in fields:
+			item[field] = price_object.get(field)
+
+		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')
+
+	def get_stock_availability(self, item):
+		"""Modify item object and add stock details."""
+		item.in_stock = False
+		warehouse = item.get("website_warehouse")
+		is_stock_item = frappe.get_cached_value("Item", item.item_code, "is_stock_item")
+
+		if item.get("on_backorder"):
+			return
+
+		if not is_stock_item:
+			if warehouse:
+				# product bundle case
+				item.in_stock = get_non_stock_item_status(item.item_code, "website_warehouse")
+			else:
+				item.in_stock = True
+		elif warehouse:
+			# stock item and has warehouse
+			actual_qty = frappe.db.get_value(
+				"Bin",
+				{"item_code": item.item_code,"warehouse": item.get("website_warehouse")},
+				"actual_qty")
+			item.in_stock = bool(flt(actual_qty))
+
+	def get_cart_items(self):
+		customer = get_customer(silent=True)
+		if customer:
+			quotation = frappe.get_all("Quotation", fields=["name"], filters=
+				{"party_name": customer, "order_type": "Shopping Cart", "docstatus": 0},
+				order_by="modified desc", limit_page_length=1)
+			if quotation:
+				items = frappe.get_all(
+					"Quotation Item",
+					fields=["item_code"],
+					filters={
+						"parent": quotation[0].get("name")
+					})
+				items = [row.item_code for row in items]
+				return items
+
+		return []
+
+	def combine_web_item_group_results(self, item_group, result, website_item_groups):
+		"""Combine results with 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)
+
+		return result
+
+	def filter_results_by_discount(self, fields, result):
+		if fields and fields.get("discount"):
+			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]
+
+		if self.filter_with_discount:
+			# no limit was added to results while querying
+			# slice results manually
+			result[:self.page_length]
+
+		return result
\ No newline at end of file
diff --git a/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py b/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py
new file mode 100644
index 0000000..f0f7918
--- /dev/null
+++ b/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py
@@ -0,0 +1,117 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import unittest
+
+import frappe
+
+from erpnext.e_commerce.api import get_product_filter_data
+from erpnext.e_commerce.doctype.website_item.test_website_item import create_regular_web_item
+
+test_dependencies = ["Item", "Item Group"]
+
+class TestItemGroupProductDataEngine(unittest.TestCase):
+	"Test Products & Sub-Category Querying for Product Listing on Item Group Page."
+
+	@classmethod
+	def setUpClass(cls):
+		item_codes = [
+			("Test Mobile A", "_Test Item Group B"),
+			("Test Mobile B", "_Test Item Group B"),
+			("Test Mobile C", "_Test Item Group B - 1"),
+			("Test Mobile D", "_Test Item Group B - 1"),
+			("Test Mobile E", "_Test Item Group B - 2")
+		]
+		for item in item_codes:
+			item_code = item[0]
+			item_args = {"item_group": item[1]}
+			if not frappe.db.exists("Website Item", {"item_code": item_code}):
+				create_regular_web_item(item_code, item_args=item_args)
+
+	@classmethod
+	def tearDownClass(cls):
+		frappe.db.rollback()
+
+	def test_product_listing_in_item_group(self):
+		"Test if only products belonging to the Item Group are fetched."
+		result = get_product_filter_data(query_args={
+			"field_filters": {},
+			"attribute_filters": {},
+			"start": 0,
+			"item_group": "_Test Item Group B"
+		})
+
+		items = result.get("items")
+		item_codes = [item.get("item_code") for item in items]
+
+		self.assertEqual(len(items), 2)
+		self.assertIn("Test Mobile A", item_codes)
+		self.assertNotIn("Test Mobile C", item_codes)
+
+	def test_products_in_multiple_item_groups(self):
+		"""Test if product is visible on multiple item group pages barring its own."""
+		website_item = frappe.get_doc("Website Item", {"item_code": "Test Mobile E"})
+
+		# show item belonging to '_Test Item Group B - 2' in '_Test Item Group B - 1' as well
+		website_item.append("website_item_groups", {
+			"item_group": "_Test Item Group B - 1"
+		})
+		website_item.save()
+
+		result = get_product_filter_data(query_args={
+			"field_filters": {},
+			"attribute_filters": {},
+			"start": 0,
+			"item_group": "_Test Item Group B - 1"
+		})
+
+		items = result.get("items")
+		item_codes = [item.get("item_code") for item in items]
+
+		self.assertEqual(len(items), 3)
+		self.assertIn("Test Mobile E", item_codes) # visible in other item groups
+		self.assertIn("Test Mobile C", item_codes)
+		self.assertIn("Test Mobile D", item_codes)
+
+		result = get_product_filter_data(query_args={
+			"field_filters": {},
+			"attribute_filters": {},
+			"start": 0,
+			"item_group": "_Test Item Group B - 2"
+		})
+
+		items = result.get("items")
+
+		self.assertEqual(len(items), 1)
+		self.assertEqual(items[0].get("item_code"), "Test Mobile E") # visible in own item group
+
+	def test_item_group_with_sub_groups(self):
+		"Test Valid Sub Item Groups in Item Group Page."
+		frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1)
+		frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 0)
+
+		result = get_product_filter_data(query_args={
+			"field_filters": {},
+			"attribute_filters": {},
+			"start": 0,
+			"item_group": "_Test Item Group B"
+		})
+
+		self.assertTrue(bool(result.get("sub_categories")))
+
+		child_groups = [d.name for d in result.get("sub_categories")]
+		# check if child group is fetched if shown in website
+		self.assertIn("_Test Item Group B - 1", child_groups)
+
+		frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 1)
+		result = get_product_filter_data(query_args={
+			"field_filters": {},
+			"attribute_filters": {},
+			"start": 0,
+			"item_group": "_Test Item Group B"
+		})
+		child_groups = [d.name for d in result.get("sub_categories")]
+
+		# check if child group is fetched if shown in website
+		self.assertIn("_Test Item Group B - 1", child_groups)
+		self.assertIn("_Test Item Group B - 2", child_groups)
\ No newline at end of file
diff --git a/erpnext/e_commerce/product_data_engine/test_product_data_engine.py b/erpnext/e_commerce/product_data_engine/test_product_data_engine.py
new file mode 100644
index 0000000..9ec336d
--- /dev/null
+++ b/erpnext/e_commerce/product_data_engine/test_product_data_engine.py
@@ -0,0 +1,350 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import unittest
+
+import frappe
+
+from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
+	setup_e_commerce_settings,
+)
+from erpnext.e_commerce.doctype.website_item.test_website_item import create_regular_web_item
+from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder
+from erpnext.e_commerce.product_data_engine.query import ProductQuery
+
+test_dependencies = ["Item", "Item Group"]
+
+class TestProductDataEngine(unittest.TestCase):
+	"Test Products Querying and Filters for Product Listing."
+
+	@classmethod
+	def setUpClass(cls):
+		item_codes = [
+			("Test 11I Laptop", "Products"), # rank 1
+			("Test 12I Laptop", "Products"), # rank 2
+			("Test 13I Laptop", "Products"), # rank 3
+			("Test 14I Laptop", "Raw Material"), # rank 4
+			("Test 15I Laptop", "Raw Material"), # rank 5
+			("Test 16I Laptop", "Raw Material"), # rank 6
+			("Test 17I Laptop", "Products") # rank 7
+		]
+		for index, item in enumerate(item_codes, start=1):
+			item_code = item[0]
+			item_args = {"item_group": item[1]}
+			web_args = {"ranking": index}
+			if not frappe.db.exists("Website Item", {"item_code": item_code}):
+				create_regular_web_item(item_code, item_args=item_args, web_args=web_args)
+
+		setup_e_commerce_settings({
+			"products_per_page": 4,
+			"enable_field_filters": 1,
+			"filter_fields": [{"fieldname": "item_group"}],
+			"enable_attribute_filters": 1,
+			"filter_attributes": [{"attribute": "Test Size"}],
+			"company": "_Test Company",
+			"enabled": 1,
+			"default_customer_group": "_Test Customer Group",
+			"price_list": "_Test Price List India"
+		})
+		frappe.local.shopping_cart_settings = None
+
+	@classmethod
+	def tearDownClass(cls):
+		frappe.db.rollback()
+
+	def test_product_list_ordering_and_paging(self):
+		"Test if website items appear by ranking on different pages."
+		engine = ProductQuery()
+		result = engine.query(
+			attributes={},
+			fields={},
+			search_term=None,
+			start=0,
+			item_group=None
+		)
+		items = result.get("items")
+
+		self.assertIsNotNone(items)
+		self.assertEqual(len(items), 4)
+		self.assertGreater(result.get("items_count"), 4)
+
+		# check if items appear as per ranking set in setUpClass
+		self.assertEqual(items[0].get("item_code"), "Test 17I Laptop")
+		self.assertEqual(items[1].get("item_code"), "Test 16I Laptop")
+		self.assertEqual(items[2].get("item_code"), "Test 15I Laptop")
+		self.assertEqual(items[3].get("item_code"), "Test 14I Laptop")
+
+		# check next page
+		result = engine.query(
+			attributes={},
+			fields={},
+			search_term=None,
+			start=4,
+			item_group=None
+		)
+		items = result.get("items")
+
+		# check if items appear as per ranking set in setUpClass on next page
+		self.assertEqual(items[0].get("item_code"), "Test 13I Laptop")
+		self.assertEqual(items[1].get("item_code"), "Test 12I Laptop")
+		self.assertEqual(items[2].get("item_code"), "Test 11I Laptop")
+
+	def test_change_product_ranking(self):
+		"Test if item on second page appear on first if ranking is changed."
+		item_code = "Test 12I Laptop"
+		old_ranking = frappe.db.get_value("Website Item", {"item_code": item_code}, "ranking")
+
+		# low rank, appears on second page
+		self.assertEqual(old_ranking, 2)
+
+		# set ranking as highest rank
+		frappe.db.set_value("Website Item", {"item_code": item_code}, "ranking", 10)
+
+		engine = ProductQuery()
+		result = engine.query(
+			attributes={},
+			fields={},
+			search_term=None,
+			start=0,
+			item_group=None
+		)
+		items = result.get("items")
+
+		# check if item is the first item on the first page
+		self.assertEqual(items[0].get("item_code"), item_code)
+		self.assertEqual(items[1].get("item_code"), "Test 17I Laptop")
+
+		# tear down
+		frappe.db.set_value("Website Item", {"item_code": item_code}, "ranking", old_ranking)
+
+	def test_product_list_field_filter_builder(self):
+		"Test if field filters are fetched correctly."
+		frappe.db.set_value("Item Group", "Raw Material", "show_in_website", 0)
+
+		filter_engine = ProductFiltersBuilder()
+		field_filters = filter_engine.get_field_filters()
+
+		# Web Items belonging to 'Products' and 'Raw Material' are available
+		# but only 'Products' has 'show_in_website' enabled
+		item_group_filters = field_filters[0]
+		docfield = item_group_filters[0]
+		valid_item_groups = item_group_filters[1]
+
+		self.assertEqual(docfield.options, "Item Group")
+		self.assertIn("Products", valid_item_groups)
+		self.assertNotIn("Raw Material", valid_item_groups)
+
+		frappe.db.set_value("Item Group", "Raw Material", "show_in_website", 1)
+		field_filters = filter_engine.get_field_filters()
+
+		#'Products' and 'Raw Materials' both have 'show_in_website' enabled
+		item_group_filters = field_filters[0]
+		docfield = item_group_filters[0]
+		valid_item_groups = item_group_filters[1]
+
+		self.assertEqual(docfield.options, "Item Group")
+		self.assertIn("Products", valid_item_groups)
+		self.assertIn("Raw Material", valid_item_groups)
+
+	def test_product_list_with_field_filter(self):
+		"Test if field filters are applied correctly."
+		field_filters = {"item_group": "Raw Material"}
+
+		engine = ProductQuery()
+		result = engine.query(
+			attributes={},
+			fields=field_filters,
+			search_term=None,
+			start=0,
+			item_group=None
+		)
+		items = result.get("items")
+
+		# check if only 'Raw Material' are fetched in the right order
+		self.assertEqual(len(items), 3)
+		self.assertEqual(items[0].get("item_code"), "Test 16I Laptop")
+		self.assertEqual(items[1].get("item_code"), "Test 15I Laptop")
+
+	# def test_product_list_with_field_filter_table_multiselect(self):
+	# 	TODO
+	# 	pass
+
+	def test_product_list_attribute_filter_builder(self):
+		"Test if attribute filters are fetched correctly."
+		create_variant_web_item()
+
+		filter_engine = ProductFiltersBuilder()
+		attribute_filter = filter_engine.get_attribute_filters()[0]
+		attribute_values = attribute_filter.item_attribute_values
+
+		self.assertEqual(attribute_filter.name, "Test Size")
+		self.assertGreater(len(attribute_values), 0)
+		self.assertIn("Large", attribute_values)
+
+	def test_product_list_with_attribute_filter(self):
+		"Test if attribute filters are applied correctly."
+		create_variant_web_item()
+
+		attribute_filters = {"Test Size": ["Large"]}
+		engine = ProductQuery()
+		result = engine.query(
+			attributes=attribute_filters,
+			fields={},
+			search_term=None,
+			start=0,
+			item_group=None
+		)
+		items = result.get("items")
+
+		# check if only items with Test Size 'Large' are fetched
+		self.assertEqual(len(items), 1)
+		self.assertEqual(items[0].get("item_code"), "Test Web Item-L")
+
+	def test_product_list_discount_filter_builder(self):
+		"Test if discount filters are fetched correctly."
+		from erpnext.e_commerce.doctype.website_item.test_website_item import (
+			make_web_item_price,
+			make_web_pricing_rule,
+		)
+
+		item_code = "Test 12I Laptop"
+		make_web_item_price(item_code=item_code)
+		make_web_pricing_rule(
+			title=f"Test Pricing Rule for {item_code}",
+			item_code=item_code,
+			selling=1
+		)
+
+		setup_e_commerce_settings({"show_price": 1})
+		frappe.local.shopping_cart_settings = None
+
+
+		engine = ProductQuery()
+		result = engine.query(
+			attributes={},
+			fields={},
+			search_term=None,
+			start=4,
+			item_group=None
+		)
+		self.assertTrue(bool(result.get("discounts")))
+
+		filter_engine = ProductFiltersBuilder()
+		discount_filters = filter_engine.get_discount_filters(result["discounts"])
+
+		self.assertEqual(len(discount_filters[0]), 2)
+		self.assertEqual(discount_filters[0][0], 10)
+		self.assertEqual(discount_filters[0][1], "10% and below")
+
+	def test_product_list_with_discount_filters(self):
+		"Test if discount filters are applied correctly."
+		from erpnext.e_commerce.doctype.website_item.test_website_item import (
+			make_web_item_price,
+			make_web_pricing_rule,
+		)
+
+		field_filters = {"discount": [10]}
+
+		make_web_item_price(item_code="Test 12I Laptop")
+		make_web_pricing_rule(
+			title="Test Pricing Rule for Test 12I Laptop", # 10% discount
+			item_code="Test 12I Laptop",
+			selling=1
+		)
+		make_web_item_price(item_code="Test 13I Laptop")
+		make_web_pricing_rule(
+			title="Test Pricing Rule for Test 13I Laptop", # 15% discount
+			item_code="Test 13I Laptop",
+			discount_percentage=15,
+			selling=1
+		)
+
+		setup_e_commerce_settings({"show_price": 1})
+		frappe.local.shopping_cart_settings = None
+
+		engine = ProductQuery()
+		result = engine.query(
+			attributes={},
+			fields=field_filters,
+			search_term=None,
+			start=0,
+			item_group=None
+		)
+		items = result.get("items")
+
+		# check if only product with 10% and below discount are fetched
+		self.assertEqual(len(items), 1)
+		self.assertEqual(items[0].get("item_code"), "Test 12I Laptop")
+
+	def test_product_list_with_api(self):
+		"Test products listing using API."
+		from erpnext.e_commerce.api import get_product_filter_data
+
+		create_variant_web_item()
+
+		result = get_product_filter_data(query_args={
+			"field_filters": {
+				"item_group": "Products"
+			},
+			"attribute_filters": {
+				"Test Size": ["Large"]
+			},
+			"start": 0
+		})
+
+		items = result.get("items")
+
+		self.assertEqual(len(items), 1)
+		self.assertEqual(items[0].get("item_code"), "Test Web Item-L")
+
+	def test_product_list_with_variants(self):
+		"Test if variants are hideen on hiding variants in settings."
+		create_variant_web_item()
+
+		setup_e_commerce_settings({
+			"enable_attribute_filters": 0,
+			"hide_variants": 1
+		})
+		frappe.local.shopping_cart_settings = None
+
+		attribute_filters = {"Test Size": ["Large"]}
+		engine = ProductQuery()
+		result = engine.query(
+			attributes=attribute_filters,
+			fields={},
+			search_term=None,
+			start=0,
+			item_group=None
+		)
+		items = result.get("items")
+
+		# check if any variants are fetched even though published variant exists
+		self.assertEqual(len(items), 0)
+
+		# tear down
+		setup_e_commerce_settings({
+			"enable_attribute_filters": 1,
+			"hide_variants": 0
+		})
+
+def create_variant_web_item():
+	"Create Variant and Template Website Items."
+	from erpnext.controllers.item_variant import create_variant
+	from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
+	from erpnext.stock.doctype.item.test_item import make_item
+
+	make_item("Test Web Item", {
+		"has_variant": 1,
+		"variant_based_on": "Item Attribute",
+		"attributes": [
+			{
+				"attribute": "Test Size"
+			}
+		]
+	})
+	if not frappe.db.exists("Item", "Test Web Item-L"):
+		variant = create_variant("Test Web Item", {"Test Size": "Large"})
+		variant.save()
+
+	if not frappe.db.exists("Website Item", {"variant_of": "Test Web Item"}):
+		make_website_item(variant, save=True)
\ No newline at end of file
diff --git a/erpnext/e_commerce/product_ui/grid.js b/erpnext/e_commerce/product_ui/grid.js
new file mode 100644
index 0000000..9eb1d45
--- /dev/null
+++ b/erpnext/e_commerce/product_ui/grid.js
@@ -0,0 +1,201 @@
+erpnext.ProductGrid = class {
+	/* Options:
+		- items: Items
+		- settings: E Commerce Settings
+		- products_section: Products Wrapper
+		- preference: If preference is not grid view, render but hide
+	*/
+	constructor(options) {
+		Object.assign(this, options);
+
+		if (this.preference !== "Grid View") {
+			this.products_section.addClass("hidden");
+		}
+
+		this.products_section.empty();
+		this.make();
+	}
+
+	make() {
+		let me = this;
+		let html = ``;
+
+		this.items.forEach(item => {
+			let title = item.web_item_name || item.item_name || item.item_code || "";
+			title =  title.length > 90 ? title.substr(0, 90) + "..." : title;
+
+			html += `<div class="col-sm-4 item-card"><div class="card text-left">`;
+			html += me.get_image_html(item, title);
+			html += me.get_card_body_html(item, title, me.settings);
+			html += `</div></div>`;
+		});
+
+		let $product_wrapper = this.products_section;
+		$product_wrapper.append(html);
+	}
+
+	get_image_html(item, title) {
+		let image = item.website_image || item.image;
+
+		if (image) {
+			return `
+				<div class="card-img-container">
+					<a href="/${ item.route || '#' }" style="text-decoration: none;">
+						<img class="card-img" src="${ image }" alt="${ title }">
+					</a>
+				</div>
+			`;
+		} else {
+			return `
+				<div class="card-img-container">
+					<a href="/${ item.route || '#' }" style="text-decoration: none;">
+						<div class="card-img-top no-image">
+							${ frappe.get_abbr(title) }
+						</div>
+					</a>
+				</div>
+			`;
+		}
+	}
+
+	get_card_body_html(item, title, settings) {
+		let body_html = `
+			<div class="card-body text-left card-body-flex" style="width:100%">
+				<div style="margin-top: 1rem; display: flex;">
+		`;
+		body_html += this.get_title(item, title);
+
+		// get floating elements
+		if (!item.has_variants) {
+			if (settings.enable_wishlist) {
+				body_html += this.get_wishlist_icon(item);
+			}
+			if (settings.enabled) {
+				body_html += this.get_cart_indicator(item);
+			}
+
+		}
+
+		body_html += `</div>`;
+		body_html += `<div class="product-category">${ item.item_group || '' }</div>`;
+
+		if (item.formatted_price) {
+			body_html += this.get_price_html(item);
+		}
+
+		body_html += this.get_stock_availability(item, settings);
+		body_html += this.get_primary_button(item, settings);
+		body_html += `</div>`; // close div on line 49
+
+		return body_html;
+	}
+
+	get_title(item, title) {
+		let title_html = `
+			<a href="/${ item.route || '#' }">
+				<div class="product-title">
+					${ title || '' }
+				</div>
+			</a>
+		`;
+		return title_html;
+	}
+
+	get_wishlist_icon(item) {
+		let icon_class = item.wished ? "wished" : "not-wished";
+		return `
+			<div class="like-action ${ item.wished ? "like-action-wished" : ''}"
+				data-item-code="${ item.item_code }">
+				<svg class="icon sm">
+					<use class="${ icon_class } wish-icon" href="#icon-heart"></use>
+				</svg>
+			</div>
+		`;
+	}
+
+	get_cart_indicator(item) {
+		return `
+			<div class="cart-indicator ${item.in_cart ? '' : 'hidden'}" data-item-code="${ item.item_code }">
+				1
+			</div>
+		`;
+	}
+
+	get_price_html(item) {
+		let price_html = `
+			<div class="product-price">
+				${ item.formatted_price || '' }
+		`;
+
+		if (item.formatted_mrp) {
+			price_html += `
+				<small class="striked-price">
+					<s>${ item.formatted_mrp ? item.formatted_mrp.replace(/ +/g, "") : "" }</s>
+				</small>
+				<small class="ml-1 product-info-green">
+					${ item.discount } OFF
+				</small>
+			`;
+		}
+		price_html += `</div>`;
+		return price_html;
+	}
+
+	get_stock_availability(item, settings) {
+		if (settings.show_stock_availability && !item.has_variants) {
+			if (item.on_backorder) {
+				return `
+					<span class="out-of-stock mb-2 mt-1" style="color: var(--primary-color)">
+						${ __("Available on backorder") }
+					</span>
+				`;
+			} else if (!item.in_stock) {
+				return `
+					<span class="out-of-stock mb-2 mt-1">
+						${ __("Out of stock") }
+					</span>
+				`;
+			}
+		}
+
+		return ``;
+	}
+
+	get_primary_button(item, settings) {
+		if (item.has_variants) {
+			return `
+				<a href="/${ item.route || '#' }">
+					<div class="btn btn-sm btn-explore-variants w-100 mt-4">
+						${ __('Explore') }
+					</div>
+				</a>
+			`;
+		} else if (settings.enabled && (settings.allow_items_not_in_stock || item.in_stock)) {
+			return `
+				<div id="${ item.name }" class="btn
+					btn-sm btn-primary btn-add-to-cart-list
+					w-100 mt-2 ${ item.in_cart ? 'hidden' : '' }"
+					data-item-code="${ item.item_code }">
+					<span class="mr-2">
+						<svg class="icon icon-md">
+							<use href="#icon-assets"></use>
+						</svg>
+					</span>
+					${ settings.enable_checkout ? __('Add to Cart') :  __('Add to Quote') }
+				</div>
+
+				<a href="/cart">
+					<div id="${ item.name }" class="btn
+						btn-sm btn-primary btn-add-to-cart-list
+						w-100 mt-4 go-to-cart-grid
+						${ item.in_cart ? '' : 'hidden' }"
+						data-item-code="${ item.item_code }">
+						${ settings.enable_checkout ? __('Go to Cart') :  __('Go to Quote') }
+					</div>
+				</a>
+			`;
+		} else {
+			return ``;
+		}
+	}
+};
\ No newline at end of file
diff --git a/erpnext/e_commerce/product_ui/list.js b/erpnext/e_commerce/product_ui/list.js
new file mode 100644
index 0000000..691cd4d
--- /dev/null
+++ b/erpnext/e_commerce/product_ui/list.js
@@ -0,0 +1,204 @@
+erpnext.ProductList = class {
+	/* Options:
+		- items: Items
+		- settings: E Commerce Settings
+		- products_section: Products Wrapper
+		- preference: If preference is not list view, render but hide
+	*/
+	constructor(options) {
+		Object.assign(this, options);
+
+		if (this.preference !== "List View") {
+			this.products_section.addClass("hidden");
+		}
+
+		this.products_section.empty();
+		this.make();
+	}
+
+	make() {
+		let me = this;
+		let html = `<br><br>`;
+
+		this.items.forEach(item => {
+			let title = item.web_item_name || item.item_name || item.item_code || "";
+			title =  title.length > 200 ? title.substr(0, 200) + "..." : title;
+
+			html += `<div class='row list-row w-100 mb-4'>`;
+			html += me.get_image_html(item, title, me.settings);
+			html += me.get_row_body_html(item, title, me.settings);
+			html += `</div>`;
+		});
+
+		let $product_wrapper = this.products_section;
+		$product_wrapper.append(html);
+	}
+
+	get_image_html(item, title, settings) {
+		let image = item.website_image || item.image;
+		let wishlist_enabled = !item.has_variants && settings.enable_wishlist;
+		let image_html = ``;
+
+		if (image) {
+			image_html += `
+				<div class="col-2 border text-center rounded list-image">
+					<a class="product-link product-list-link" href="/${ item.route || '#' }">
+						<img itemprop="image" class="website-image h-100 w-100" alt="${ title }"
+							src="${ image }">
+					</a>
+					${ wishlist_enabled ? this.get_wishlist_icon(item): '' }
+				</div>
+			`;
+		} else {
+			image_html += `
+				<div class="col-2 border text-center rounded list-image">
+					<a class="product-link product-list-link" href="/${ item.route || '#' }"
+						style="text-decoration: none">
+						<div class="card-img-top no-image-list">
+							${ frappe.get_abbr(title) }
+						</div>
+					</a>
+					${ wishlist_enabled ? this.get_wishlist_icon(item): '' }
+				</div>
+			`;
+		}
+
+		return image_html;
+	}
+
+	get_row_body_html(item, title, settings) {
+		let body_html = `<div class='col-10 text-left'>`;
+		body_html += this.get_title_html(item, title, settings);
+		body_html += this.get_item_details(item, settings);
+		body_html += `</div>`;
+		return body_html;
+	}
+
+	get_title_html(item, title, settings) {
+		let title_html = `<div style="display: flex; margin-left: -15px;">`;
+		title_html += `
+			<div class="col-8" style="margin-right: -15px;">
+				<a class="" href="/${ item.route || '#' }"
+					style="color: var(--gray-800); font-weight: 500;">
+					${ title }
+				</a>
+			</div>
+		`;
+
+		if (settings.enabled) {
+			title_html += `<div class="col-4 cart-action-container ${item.in_cart ? 'd-flex' : ''}">`;
+			title_html += this.get_primary_button(item, settings);
+			title_html += `</div>`;
+		}
+		title_html += `</div>`;
+
+		return title_html;
+	}
+
+	get_item_details(item, settings) {
+		let details = `
+			<p class="product-code">
+				${ item.item_group } | Item Code : ${ item.item_code }
+			</p>
+			<div class="mt-2" style="color: var(--gray-600) !important; font-size: 13px;">
+				${ item.short_description || '' }
+			</div>
+			<div class="product-price">
+				${ item.formatted_price || '' }
+		`;
+
+		if (item.formatted_mrp) {
+			details += `
+				<small class="striked-price">
+					<s>${ item.formatted_mrp ? item.formatted_mrp.replace(/ +/g, "") : "" }</s>
+				</small>
+				<small class="ml-1 product-info-green">
+					${ item.discount } OFF
+				</small>
+			`;
+		}
+
+		details += this.get_stock_availability(item, settings);
+		details += `</div>`;
+
+		return details;
+	}
+
+	get_stock_availability(item, settings) {
+		if (settings.show_stock_availability && !item.has_variants) {
+			if (item.on_backorder) {
+				return `
+					<br>
+					<span class="out-of-stock mt-2" style="color: var(--primary-color)">
+						${ __("Available on backorder") }
+					</span>
+				`;
+			} else if (!item.in_stock) {
+				return `
+					<br>
+					<span class="out-of-stock mt-2">${ __("Out of stock") }</span>
+				`;
+			}
+		}
+		return ``;
+	}
+
+	get_wishlist_icon(item) {
+		let icon_class = item.wished ? "wished" : "not-wished";
+
+		return `
+			<div class="like-action-list ${ item.wished ? "like-action-wished" : ''}"
+				data-item-code="${ item.item_code }">
+				<svg class="icon sm">
+					<use class="${ icon_class } wish-icon" href="#icon-heart"></use>
+				</svg>
+			</div>
+		`;
+	}
+
+	get_primary_button(item, settings) {
+		if (item.has_variants) {
+			return `
+				<a href="/${ item.route || '#' }">
+					<div class="btn btn-sm btn-explore-variants btn mb-0 mt-0">
+						${ __('Explore') }
+					</div>
+				</a>
+			`;
+		} else if (settings.enabled && (settings.allow_items_not_in_stock || item.in_stock)) {
+			return `
+				<div id="${ item.name }" class="btn
+					btn-sm btn-primary btn-add-to-cart-list mb-0
+					${ item.in_cart ? 'hidden' : '' }"
+					data-item-code="${ item.item_code }"
+					style="margin-top: 0px !important; max-height: 30px; float: right;
+						padding: 0.25rem 1rem; min-width: 135px;">
+					<span class="mr-2">
+						<svg class="icon icon-md">
+							<use href="#icon-assets"></use>
+						</svg>
+					</span>
+					${ settings.enable_checkout ? __('Add to Cart') :  __('Add to Quote') }
+				</div>
+
+				<div class="cart-indicator list-indicator ${item.in_cart ? '' : 'hidden'}">
+					1
+				</div>
+
+				<a href="/cart">
+					<div id="${ item.name }" class="btn
+						btn-sm btn-primary btn-add-to-cart-list
+						ml-4 go-to-cart mb-0 mt-0
+						${ item.in_cart ? '' : 'hidden' }"
+						data-item-code="${ item.item_code }"
+						style="padding: 0.25rem 1rem; min-width: 135px;">
+						${ settings.enable_checkout ? __('Go to Cart') :  __('Go to Quote') }
+					</div>
+				</a>
+			`;
+		} else {
+			return ``;
+		}
+	}
+
+};
\ No newline at end of file
diff --git a/erpnext/e_commerce/product_ui/search.js b/erpnext/e_commerce/product_ui/search.js
new file mode 100644
index 0000000..6192245
--- /dev/null
+++ b/erpnext/e_commerce/product_ui/search.js
@@ -0,0 +1,244 @@
+erpnext.ProductSearch = class {
+	constructor(opts) {
+		/* Options: search_box_id (for custom search box) */
+		$.extend(this, opts);
+		this.MAX_RECENT_SEARCHES = 4;
+		this.search_box_id = this.search_box_id || "#search-box";
+		this.searchBox = $(this.search_box_id);
+
+		this.setupSearchDropDown();
+		this.bindSearchAction();
+	}
+
+	setupSearchDropDown() {
+		this.search_area = $("#dropdownMenuSearch");
+		this.setupSearchResultContainer();
+		this.populateRecentSearches();
+	}
+
+	bindSearchAction() {
+		let me = this;
+
+		// Show Search dropdown
+		this.searchBox.on("focus", () => {
+			this.search_dropdown.removeClass("hidden");
+		});
+
+		// If click occurs outside search input/results, hide results.
+		// Click can happen anywhere on the page
+		$("body").on("click", (e) => {
+			let searchEvent = $(e.target).closest(this.search_box_id).length;
+			let resultsEvent = $(e.target).closest('#search-results-container').length;
+			let isResultHidden = this.search_dropdown.hasClass("hidden");
+
+			if (!searchEvent && !resultsEvent && !isResultHidden) {
+				this.search_dropdown.addClass("hidden");
+			}
+		});
+
+		// Process search input
+		this.searchBox.on("input", (e) => {
+			let query = e.target.value;
+
+			if (query.length == 0) {
+				me.populateResults(null);
+				me.populateCategoriesList(null);
+			}
+
+			if (query.length < 3 || !query.length) return;
+
+			frappe.call({
+				method: "erpnext.templates.pages.product_search.search",
+				args: {
+					query: query
+				},
+				callback: (data) => {
+					let product_results = null, category_results = null;
+
+					// Populate product results
+					product_results = data.message ? data.message.product_results : null;
+					me.populateResults(product_results);
+
+					// Populate categories
+					if (me.category_container) {
+						category_results = data.message ? data.message.category_results : null;
+						me.populateCategoriesList(category_results);
+					}
+
+					// Populate recent search chips only on successful queries
+					if (!$.isEmptyObject(product_results) || !$.isEmptyObject(category_results)) {
+						me.setRecentSearches(query);
+					}
+				}
+			});
+
+			this.search_dropdown.removeClass("hidden");
+		});
+	}
+
+	setupSearchResultContainer() {
+		this.search_dropdown = this.search_area.append(`
+			<div class="overflow-hidden shadow dropdown-menu w-100 hidden"
+				id="search-results-container"
+				aria-labelledby="dropdownMenuSearch"
+				style="display: flex; flex-direction: column;">
+			</div>
+		`).find("#search-results-container");
+
+		this.setupCategoryContainer();
+		this.setupProductsContainer();
+		this.setupRecentsContainer();
+	}
+
+	setupProductsContainer() {
+		this.products_container = this.search_dropdown.append(`
+			<div id="product-results mt-2">
+				<div id="product-scroll" style="overflow: scroll; max-height: 300px">
+				</div>
+			</div>
+		`).find("#product-scroll");
+	}
+
+	setupCategoryContainer() {
+		this.category_container = this.search_dropdown.append(`
+			<div class="category-container mt-2 mb-1">
+				<div class="category-chips">
+				</div>
+			</div>
+		`).find(".category-chips");
+	}
+
+	setupRecentsContainer() {
+		let $recents_section = this.search_dropdown.append(`
+			<div class="mb-2 mt-2 recent-searches">
+				<div>
+					<b>${ __("Recent") }</b>
+				</div>
+			</div>
+		`).find(".recent-searches");
+
+		this.recents_container = $recents_section.append(`
+			<div id="recents" style="padding: .25rem 0 1rem 0;">
+			</div>
+		`).find("#recents");
+	}
+
+	getRecentSearches() {
+		return JSON.parse(localStorage.getItem("recent_searches") || "[]");
+	}
+
+	attachEventListenersToChips() {
+		let me  = this;
+		const chips = $(".recent-search");
+		window.chips = chips;
+
+		for (let chip of chips) {
+			chip.addEventListener("click", () => {
+				me.searchBox[0].value = chip.innerText.trim();
+
+				// Start search with `recent query`
+				me.searchBox.trigger("input");
+				me.searchBox.focus();
+			});
+		}
+	}
+
+	setRecentSearches(query) {
+		let recents = this.getRecentSearches();
+		if (recents.length >= this.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));
+
+		this.populateRecentSearches();
+	}
+
+	populateRecentSearches() {
+		let recents = this.getRecentSearches();
+
+		if (!recents.length) {
+			this.recents_container.html(`<span class=""text-muted">No searches yet.</span>`);
+			return;
+		}
+
+		let html = "";
+		recents.forEach((key) => {
+			html += `
+				<div class="recent-search mr-1" style="font-size: 13px">
+					<span class="mr-2">
+						<svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+							<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="var(--gray-500)"" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
+							<path d="M8.00027 5.20947V8.00017L10 10" stroke="var(--gray-500)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
+						</svg>
+					</span>
+					${ key }
+				</div>
+			`;
+		});
+
+		this.recents_container.html(html);
+		this.attachEventListenersToChips();
+	}
+
+	populateResults(product_results) {
+		if (!product_results || product_results.length === 0) {
+			let empty_html = ``;
+			this.products_container.html(empty_html);
+			return;
+		}
+
+		let html = "";
+
+		product_results.forEach((res) => {
+			let thumbnail = res.thumbnail || '/assets/erpnext/images/ui-states/cart-empty-state.png';
+			html += `
+				<div class="dropdown-item" style="display: flex;">
+					<img class="item-thumb col-2" src=${thumbnail} />
+					<div class="col-9" style="white-space: normal;">
+						<a href="/${res.route}">${res.web_item_name}</a><br>
+						<span class="brand-line">${res.brand ? "by " + res.brand : ""}</span>
+					</div>
+				</div>
+			`;
+		});
+
+		this.products_container.html(html);
+	}
+
+	populateCategoriesList(category_results) {
+		if (!category_results || category_results.length === 0) {
+			let empty_html = `
+				<div class="category-container mt-2">
+					<div class="category-chips">
+					</div>
+				</div>
+			`;
+			this.category_container.html(empty_html);
+			return;
+		}
+
+		let html = `
+			<div class="mb-2">
+				<b>${ __("Categories") }</b>
+			</div>
+		`;
+
+		category_results.forEach((category) => {
+			html += `
+				<a href="/${category.route}" class="btn btn-sm category-chip mr-2 mb-2"
+					style="font-size: 13px" role="button">
+				${ category.name }
+				</button>
+			`;
+		});
+
+		this.category_container.html(html);
+	}
+};
\ No newline at end of file
diff --git a/erpnext/e_commerce/product_ui/views.js b/erpnext/e_commerce/product_ui/views.js
new file mode 100644
index 0000000..1b5c440
--- /dev/null
+++ b/erpnext/e_commerce/product_ui/views.js
@@ -0,0 +1,532 @@
+erpnext.ProductView =  class {
+	/* Options:
+		- View Type
+		- Products Section Wrapper,
+		- Item Group: If its an Item Group page
+	*/
+	constructor(options) {
+		Object.assign(this, options);
+		this.preference = this.view_type;
+		this.make();
+	}
+
+	make(from_filters=false) {
+		this.products_section.empty();
+		this.prepare_toolbar();
+		this.get_item_filter_data(from_filters);
+	}
+
+	prepare_toolbar() {
+		this.products_section.append(`
+			<div class="toolbar d-flex">
+			</div>
+		`);
+		this.prepare_search();
+		this.prepare_view_toggler();
+
+		new erpnext.ProductSearch();
+	}
+
+	prepare_view_toggler() {
+
+		if (!$("#list").length || !$("#image-view").length) {
+			this.render_view_toggler();
+			this.bind_view_toggler_actions();
+			this.set_view_state();
+		}
+	}
+
+	get_item_filter_data(from_filters=false) {
+		// Get and render all Product related views
+		let me = this;
+		this.from_filters = from_filters;
+		let args = this.get_query_filters();
+
+		this.disable_view_toggler(true);
+
+		frappe.call({
+			method: "erpnext.e_commerce.api.get_product_filter_data",
+			args: {
+				query_args: args
+			},
+			callback: function(result) {
+				if (!result || result.exc || !result.message || result.message.exc) {
+					me.render_no_products_section(true);
+				} else {
+					// Sub Category results are independent of Items
+					if (me.item_group && result.message["sub_categories"].length) {
+						me.render_item_sub_categories(result.message["sub_categories"]);
+					}
+
+					if (!result.message["items"].length) {
+						// if result has no items or result is empty
+						me.render_no_products_section();
+					} else {
+						// Add discount filters
+						me.re_render_discount_filters(result.message["filters"].discount_filters);
+
+						// Render views
+						me.render_list_view(result.message["items"], result.message["settings"]);
+						me.render_grid_view(result.message["items"], result.message["settings"]);
+
+						me.products = result.message["items"];
+						me.product_count = result.message["items_count"];
+					}
+
+					// Bind filter actions
+					if (!from_filters) {
+						// If `get_product_filter_data` was triggered after checking a filter,
+						// don't touch filters unnecessarily, only data must change
+						// filter persistence is handle on filter change event
+						me.bind_filters();
+						me.restore_filters_state();
+					}
+
+					// Bottom paging
+					me.add_paging_section(result.message["settings"]);
+				}
+
+				me.disable_view_toggler(false);
+			}
+		});
+	}
+
+	disable_view_toggler(disable=false) {
+		$('#list').prop('disabled', disable);
+		$('#image-view').prop('disabled', disable);
+	}
+
+	render_grid_view(items, settings) {
+		// loop over data and add grid html to it
+		let me = this;
+		this.prepare_product_area_wrapper("grid");
+
+		new erpnext.ProductGrid({
+			items: items,
+			products_section: $("#products-grid-area"),
+			settings: settings,
+			preference: me.preference
+		});
+	}
+
+	render_list_view(items, settings) {
+		let me = this;
+		this.prepare_product_area_wrapper("list");
+
+		new erpnext.ProductList({
+			items: items,
+			products_section: $("#products-list-area"),
+			settings: settings,
+			preference: me.preference
+		});
+	}
+
+	prepare_product_area_wrapper(view) {
+		let left_margin = view == "list" ? "ml-2" : "";
+		let top_margin = view == "list" ? "mt-6" : "mt-minus-1";
+		return this.products_section.append(`
+			<br>
+			<div id="products-${view}-area" class="row products-list ${ top_margin } ${ left_margin }"></div>
+		`);
+	}
+
+	get_query_filters() {
+		const filters = frappe.utils.get_query_params();
+		let {field_filters, attribute_filters} = filters;
+
+		field_filters = field_filters ? JSON.parse(field_filters) : {};
+		attribute_filters = attribute_filters ? JSON.parse(attribute_filters) : {};
+
+		return {
+			field_filters: field_filters,
+			attribute_filters: attribute_filters,
+			item_group: this.item_group,
+			start: filters.start || null,
+			from_filters: this.from_filters || false
+		};
+	}
+
+	add_paging_section(settings) {
+		$(".product-paging-area").remove();
+
+		if (this.products) {
+			let paging_html = `
+				<div class="row product-paging-area mt-5">
+					<div class="col-3">
+					</div>
+					<div class="col-9 text-right">
+			`;
+			let query_params = frappe.utils.get_query_params();
+			let start = query_params.start ? cint(JSON.parse(query_params.start)) : 0;
+			let page_length = settings.products_per_page || 0;
+
+			let prev_disable = start > 0 ? "" : "disabled";
+			let next_disable = (this.product_count > page_length) ? "" : "disabled";
+
+			paging_html += `
+				<button class="btn btn-default btn-prev" data-start="${ start - page_length }"
+					style="float: left" ${prev_disable}>
+					${ __("Prev") }
+				</button>`;
+
+			paging_html += `
+				<button class="btn btn-default btn-next" data-start="${ start + page_length }"
+					${next_disable}>
+					${ __("Next") }
+				</button>
+			`;
+
+			paging_html += `</div></div>`;
+
+			$(".page_content").append(paging_html);
+			this.bind_paging_action();
+		}
+	}
+
+	prepare_search() {
+		$(".toolbar").append(`
+			<div class="input-group col-8 p-0">
+				<div class="dropdown w-100" id="dropdownMenuSearch">
+					<input type="search" name="query" id="search-box" class="form-control font-md"
+						placeholder="Search for Products"
+						aria-label="Product" aria-describedby="button-addon2">
+					<div class="search-icon">
+						<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
+							fill="none"
+							stroke="currentColor" stroke-width="2" stroke-linecap="round"
+							stroke-linejoin="round"
+							class="feather feather-search">
+							<circle cx="11" cy="11" r="8"></circle>
+							<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
+						</svg>
+					</div>
+					<!-- Results dropdown rendered in product_search.js -->
+				</div>
+			</div>
+		`);
+	}
+
+	render_view_toggler() {
+		$(".toolbar").append(`<div class="toggle-container col-4 p-0"></div>`);
+
+		["btn-list-view", "btn-grid-view"].forEach(view => {
+			let icon = view === "btn-list-view" ? "list" : "image-view";
+			$(".toggle-container").append(`
+				<div class="form-group mb-0" id="toggle-view">
+					<button id="${ icon }" class="btn ${ view } mr-2">
+						<span>
+							<svg class="icon icon-md">
+								<use href="#icon-${ icon }"></use>
+							</svg>
+						</span>
+					</button>
+				</div>
+			`);
+		});
+	}
+
+	bind_view_toggler_actions() {
+		$("#list").click(function() {
+			let $btn = $(this);
+			$btn.removeClass('btn-primary');
+			$btn.addClass('btn-primary');
+			$(".btn-grid-view").removeClass('btn-primary');
+
+			$("#products-grid-area").addClass("hidden");
+			$("#products-list-area").removeClass("hidden");
+			localStorage.setItem("product_view", "List View");
+		});
+
+		$("#image-view").click(function() {
+			let $btn = $(this);
+			$btn.removeClass('btn-primary');
+			$btn.addClass('btn-primary');
+			$(".btn-list-view").removeClass('btn-primary');
+
+			$("#products-list-area").addClass("hidden");
+			$("#products-grid-area").removeClass("hidden");
+			localStorage.setItem("product_view", "Grid View");
+		});
+	}
+
+	set_view_state() {
+		if (this.preference === "List View") {
+			$("#list").addClass('btn-primary');
+			$("#image-view").removeClass('btn-primary');
+		} else {
+			$("#image-view").addClass('btn-primary');
+			$("#list").removeClass('btn-primary');
+		}
+	}
+
+	bind_paging_action() {
+		let me = this;
+		$('.btn-prev, .btn-next').click((e) => {
+			const $btn = $(e.target);
+			me.from_filters = false;
+
+			$btn.prop('disabled', true);
+			const start = $btn.data('start');
+
+			let query_params = frappe.utils.get_query_params();
+			query_params.start = start;
+			let path = window.location.pathname + '?' + frappe.utils.get_url_from_dict(query_params);
+			window.location.href = path;
+		});
+	}
+
+	re_render_discount_filters(filter_data) {
+		this.get_discount_filter_html(filter_data);
+		if (this.from_filters) {
+			// Bind filter action if triggered via filters
+			// if not from filter action, page load will bind actions
+			this.bind_discount_filter_action();
+		}
+		// discount filters are rendered with Items (later)
+		// unlike the other filters
+		this.restore_discount_filter();
+	}
+
+	get_discount_filter_html(filter_data) {
+		$("#discount-filters").remove();
+		if (filter_data) {
+			$("#product-filters").append(`
+				<div id="discount-filters" class="mb-4 filter-block pb-5">
+					<div class="filter-label mb-3">${ __("Discounts") }</div>
+				</div>
+			`);
+
+			let html = `<div class="filter-options">`;
+			filter_data.forEach(filter => {
+				html += `
+					<div class="checkbox">
+						<label data-value="${ filter[0] }">
+							<input type="radio"
+								class="product-filter discount-filter"
+								name="discount" id="${ filter[0] }"
+								data-filter-name="discount"
+								data-filter-value="${ filter[0] }"
+								style="width: 14px !important"
+							>
+								<span class="label-area" for="${ filter[0] }">
+									${ filter[1] }
+								</span>
+						</label>
+					</div>
+				`;
+			});
+			html += `</div>`;
+
+			$("#discount-filters").append(html);
+		}
+	}
+
+	restore_discount_filter() {
+		const filters = frappe.utils.get_query_params();
+		let field_filters = filters.field_filters;
+		if (!field_filters) return;
+
+		field_filters = JSON.parse(field_filters);
+
+		if (field_filters && field_filters["discount"]) {
+			const values = field_filters["discount"];
+			const selector = values.map(value => {
+				return `input[data-filter-name="discount"][data-filter-value="${value}"]`;
+			}).join(',');
+			$(selector).prop('checked', true);
+			this.field_filters = field_filters;
+		}
+	}
+
+	bind_discount_filter_action() {
+		let me = this;
+		$('.discount-filter').on('change', (e) => {
+			const $checkbox = $(e.target);
+			const is_checked = $checkbox.is(':checked');
+
+			const {
+				filterValue: filter_value
+			} = $checkbox.data();
+
+			delete this.field_filters["discount"];
+
+			if (is_checked) {
+				this.field_filters["discount"] = [];
+				this.field_filters["discount"].push(filter_value);
+			}
+
+			if (this.field_filters["discount"].length === 0) {
+				delete this.field_filters["discount"];
+			}
+
+			me.change_route_with_filters();
+		});
+	}
+
+	bind_filters() {
+		let me = this;
+		this.field_filters = {};
+		this.attribute_filters = {};
+
+		$('.product-filter').on('change', (e) => {
+			me.from_filters = true;
+
+			const $checkbox = $(e.target);
+			const is_checked = $checkbox.is(':checked');
+
+			if ($checkbox.is('.attribute-filter')) {
+				const {
+					attributeName: attribute_name,
+					attributeValue: attribute_value
+				} = $checkbox.data();
+
+				if (is_checked) {
+					this.attribute_filters[attribute_name] = this.attribute_filters[attribute_name] || [];
+					this.attribute_filters[attribute_name].push(attribute_value);
+				} else {
+					this.attribute_filters[attribute_name] = this.attribute_filters[attribute_name] || [];
+					this.attribute_filters[attribute_name] = this.attribute_filters[attribute_name].filter(v => v !== attribute_value);
+				}
+
+				if (this.attribute_filters[attribute_name].length === 0) {
+					delete this.attribute_filters[attribute_name];
+				}
+			} 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] || [];
+					if (!in_list(this.field_filters[filter_name], filter_value)) {
+						this.field_filters[filter_name].push(filter_value);
+					}
+				} else {
+					this.field_filters[filter_name] = this.field_filters[filter_name] || [];
+					this.field_filters[filter_name] = this.field_filters[filter_name].filter(v => v !== filter_value);
+				}
+
+				if (this.field_filters[filter_name].length === 0) {
+					delete this.field_filters[filter_name];
+				}
+			}
+
+			me.change_route_with_filters();
+		});
+	}
+
+	change_route_with_filters() {
+		let route_params = frappe.utils.get_query_params();
+
+		let start = this.if_key_exists(route_params.start) || 0;
+		if (this.from_filters) {
+			start = 0; // show items from first page if new filters are triggered
+		}
+
+		const query_string = this.get_query_string({
+			start: start,
+			field_filters: JSON.stringify(this.if_key_exists(this.field_filters)),
+			attribute_filters: JSON.stringify(this.if_key_exists(this.attribute_filters)),
+		});
+		window.history.pushState('filters', '', `${location.pathname}?` + query_string);
+
+		$('.page_content input').prop('disabled', true);
+
+		this.make(true);
+		$('.page_content input').prop('disabled', false);
+	}
+
+	restore_filters_state() {
+		const filters = frappe.utils.get_query_params();
+		let {field_filters, attribute_filters} = filters;
+
+		if (field_filters) {
+			field_filters = JSON.parse(field_filters);
+			for (let fieldname in field_filters) {
+				const values = field_filters[fieldname];
+				const selector = values.map(value => {
+					return `input[data-filter-name="${fieldname}"][data-filter-value="${value}"]`;
+				}).join(',');
+				$(selector).prop('checked', true);
+			}
+			this.field_filters = field_filters;
+		}
+		if (attribute_filters) {
+			attribute_filters = JSON.parse(attribute_filters);
+			for (let attribute in attribute_filters) {
+				const values = attribute_filters[attribute];
+				const selector = values.map(value => {
+					return `input[data-attribute-name="${attribute}"][data-attribute-value="${value}"]`;
+				}).join(',');
+				$(selector).prop('checked', true);
+			}
+			this.attribute_filters = attribute_filters;
+		}
+	}
+
+	render_no_products_section(error=false) {
+		let error_section = `
+			<div class="mt-4 w-100 alert alert-error font-md">
+				Something went wrong. Please refresh or contact us.
+			</div>
+		`;
+		let no_results_section = `
+			<div class="cart-empty frappe-card mt-4">
+				<div class="cart-empty-state">
+					<img src="/assets/erpnext/images/ui-states/cart-empty-state.png" alt="Empty Cart">
+				</div>
+				<div class="cart-empty-message mt-4">${ __('No products found') }</p>
+			</div>
+		`;
+
+		this.products_section.append(error ? error_section : no_results_section);
+	}
+
+	render_item_sub_categories(categories) {
+		if (categories && categories.length) {
+			let sub_group_html = `
+				<div class="sub-category-container scroll-categories">
+			`;
+
+			categories.forEach(category => {
+				sub_group_html += `
+					<a href="${ category.route || '#' }" style="text-decoration: none;">
+						<div class="category-pill">
+							${ category.name }
+						</div>
+					</a>
+				`;
+			});
+			sub_group_html += `</div>`;
+
+			$("#product-listing").prepend(sub_group_html);
+		}
+	}
+
+	get_query_string(object) {
+		const url = new URLSearchParams();
+		for (let key in object) {
+			const value = object[key];
+			if (value) {
+				url.append(key, value);
+			}
+		}
+		return url.toString();
+	}
+
+	if_key_exists(obj) {
+		let exists = false;
+		for (let key in obj) {
+			if (Object.prototype.hasOwnProperty.call(obj, key) && obj[key]) {
+				exists = true;
+				break;
+			}
+		}
+		return exists ? obj : undefined;
+	}
+};
\ No newline at end of file
diff --git a/erpnext/e_commerce/redisearch_utils.py b/erpnext/e_commerce/redisearch_utils.py
new file mode 100644
index 0000000..59c7f32
--- /dev/null
+++ b/erpnext/e_commerce/redisearch_utils.py
@@ -0,0 +1,210 @@
+# Copyright (c) 2021, 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 AutoCompleter, Client, IndexDefinition, Suggestion, TagField, TextField
+
+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'
+
+def get_indexable_web_fields():
+	"Return valid fields from Website Item that can be searched for."
+	web_item_meta = frappe.get_meta("Website Item", cached=True)
+	valid_fields = filter(
+		lambda df: df.fieldtype in ("Link", "Table MultiSelect", "Data", "Small Text", "Text Editor"),
+		web_item_meta.fields)
+
+	return [df.fieldname for df in valid_fields]
+
+def is_search_module_loaded():
+	try:
+		cache = frappe.cache()
+		out = cache.execute_command('MODULE LIST')
+
+		parsed_output = " ".join(
+			(" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out)
+		)
+		return "search" in parsed_output
+	except Exception:
+		return False
+
+def if_redisearch_loaded(function):
+	"Decorator to check if Redisearch is loaded."
+	def wrapper(*args, **kwargs):
+		if is_search_module_loaded():
+			func = function(*args, **kwargs)
+			return func
+		return
+
+	return wrapper
+
+def make_key(key):
+	return "{0}|{1}".format(frappe.conf.db_name, key).encode('utf-8')
+
+@if_redisearch_loaded
+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 Exception:
+		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'
+	)
+	idx_fields = idx_fields.split(',') if idx_fields else []
+
+	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)
+
+@if_redisearch_loaded
+def insert_item_to_index(website_item_doc):
+	# Insert item to index
+	key = get_cache_key(website_item_doc.name)
+	cache = frappe.cache()
+	web_item = create_web_item_map(website_item_doc)
+
+	for k, v in web_item.items():
+		super(RedisWrapper, cache).hset(make_key(key), k, v)
+
+	insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name)
+
+@if_redisearch_loaded
+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
+
+@if_redisearch_loaded
+def update_index_for_item(website_item_doc):
+	# Reinsert to Cache
+	insert_item_to_index(website_item_doc)
+	define_autocomplete_dictionary()
+
+@if_redisearch_loaded
+def delete_item_from_index(website_item_doc):
+	cache = frappe.cache()
+	key = get_cache_key(website_item_doc.name)
+
+	try:
+		cache.delete(key)
+	except Exception:
+		return False
+
+	delete_from_ac_dict(website_item_doc)
+	return True
+
+@if_redisearch_loaded
+def delete_from_ac_dict(website_item_doc):
+	'''Removes this items's name from autocomplete dictionary'''
+	cache = frappe.cache()
+	name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
+	name_ac.delete(website_item_doc.web_item_name)
+
+@if_redisearch_loaded
+def define_autocomplete_dictionary():
+	"""Creates an autocomplete search dictionary for `name`.
+		Also creats autocomplete dictionary for `categories` if
+		checked in E Commerce Settings"""
+
+	cache = frappe.cache()
+	name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
+	cat_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=cache)
+
+	ac_categories = frappe.db.get_single_value(
+		'E Commerce Settings',
+		'show_categories_in_search_autocomplete'
+	)
+
+	# Delete both autocomplete dicts
+	try:
+		cache.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE))
+		cache.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE))
+	except Exception:
+		return False
+
+	items = frappe.get_all(
+		'Website Item',
+		fields=['web_item_name', 'item_group'],
+		filters={"published": 1}
+	)
+
+	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
+
+@if_redisearch_loaded
+def reindex_all_web_items():
+	items = frappe.get_all(
+		'Website Item',
+		fields=get_fields_indexed(),
+		filters={"published": True}
+	)
+
+	cache = 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, cache).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'
+	)
+	fields_to_index = fields_to_index.split(',') if fields_to_index else []
+
+	mandatory_fields = ['name', 'web_item_name', 'route', 'thumbnail', 'ranking']
+	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/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 87%
rename from erpnext/shopping_cart/cart.py
rename to erpnext/e_commerce/shopping_cart/cart.py
index ebbe233..458cf69 100644
--- a/erpnext/shopping_cart/cart.py
+++ b/erpnext/e_commerce/shopping_cart/cart.py
@@ -1,7 +1,6 @@
 # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
-
 import frappe
 import frappe.defaults
 from frappe import _, throw
@@ -11,20 +10,20 @@
 from frappe.utils.nestedset import get_root_of
 
 from erpnext.accounts.utils import get_account_name
-from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
+from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_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")))
+		cart_count = cstr(cint(quotation.get("total_qty")))
 
 		if hasattr(frappe.local, "cookie_manager"):
 			frappe.local.cookie_manager.set_cookie("cart_count", cart_count)
@@ -48,7 +47,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()
@@ -72,7 +71,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
 
@@ -92,13 +91,19 @@
 
 	if not cint(cart_settings.allow_items_not_in_stock):
 		for item in sales_order.get("items"):
-			item.reserved_warehouse, is_stock_item = frappe.db.get_value("Item",
-				item.item_code, ["website_warehouse", "is_stock_item"])
+			item.warehouse = frappe.db.get_value(
+				"Website Item",
+				{
+					"item_code": item.item_code
+				},
+				"website_warehouse"
+			)
+			is_stock_item = frappe.db.get_value("Item", item.item_code, "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))
+					throw(_("{0} Not in Stock").format(item.item_code))
 				if item.qty > item_stock.stock_qty[0][0]:
 					throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code))
 
@@ -156,19 +161,19 @@
 
 	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),
-			"taxes": frappe.render_template("templates/includes/order/order_taxes.html",
+			"total": frappe.render_template("templates/includes/cart/cart_items_total.html",
 				context),
+			"taxes_and_totals": frappe.render_template("templates/includes/cart/cart_payment_summary.html",
+				context)
 		}
 	else:
 		return {
-			'name': quotation.name,
-			'shopping_cart_menu': get_shopping_cart_menu(context)
+			'name': quotation.name
 		}
 
 @frappe.whitelist()
@@ -265,13 +270,36 @@
 		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,
-			["thumbnail", "website_image", "description", "route"], as_dict=True))
+		item_code = d.item_code
+		fields = ["web_item_name", "thumbnail", "website_image", "description", "route"]
+
+		# Variant Item
+		if not frappe.db.exists("Website Item", {"item_code": item_code}):
+			variant_data = frappe.db.get_values(
+				"Item",
+				filters={"item_code": item_code},
+				fieldname=["variant_of", "item_name", "image"],
+				as_dict=True
+			)[0]
+			item_code = variant_data.variant_of
+			fields = fields[1:]
+			d.web_item_name = variant_data.item_name
+
+			if variant_data.image: # get image from variant or template web item
+				d.thumbnail = variant_data.image
+				fields = fields[2:]
+
+		d.update(frappe.db.get_value(
+			"Website Item",
+			{"item_code": item_code},
+			fields,
+			as_dict=True)
+		)
 
 	return doc
 
@@ -288,7 +316,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-",
@@ -343,7 +371,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)
 
@@ -420,7 +448,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 = ''
 
@@ -557,10 +585,20 @@
 	if quotation.shipping_address_name:
 		country = frappe.db.get_value("Address", quotation.shipping_address_name, "country")
 		if country:
-			shipping_rules = frappe.db.sql_list("""select distinct sr.name
-				from `tabShipping Rule Country` src, `tabShipping Rule` sr
-				where src.country = %s and
-				sr.disabled != 1 and sr.name = src.parent""", country)
+			sr_country = frappe.qb.DocType("Shipping Rule Country")
+			sr = frappe.qb.DocType("Shipping Rule")
+			query = (
+				frappe.qb.from_(sr_country)
+				.join(sr).on(sr.name == sr_country.parent)
+				.select(sr.name)
+				.distinct()
+				.where(
+					(sr_country.country == country)
+					& (sr.disabled != 1)
+				)
+			)
+			result = query.run(as_list=True)
+			shipping_rules = [x[0] for x in result]
 
 	return shipping_rules
 
diff --git a/erpnext/e_commerce/shopping_cart/product_info.py b/erpnext/e_commerce/shopping_cart/product_info.py
new file mode 100644
index 0000000..595fed0
--- /dev/null
+++ b/erpnext/e_commerce/shopping_cart/product_info.py
@@ -0,0 +1,97 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+
+from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
+	get_shopping_cart_settings,
+	show_quantity_in_website,
+)
+from erpnext.e_commerce.shopping_cart.cart import _get_cart_quotation, _set_price_list
+from erpnext.utilities.product import (
+	get_non_stock_item_status,
+	get_price,
+	get_web_item_qty_in_stock,
+)
+
+
+@frappe.whitelist(allow_guest=True)
+def get_product_info_for_website(item_code, skip_quotation_creation=False):
+	"""get product price / stock info for website"""
+
+	cart_settings = get_shopping_cart_settings()
+	if not cart_settings.enabled:
+		# return settings even if cart is disabled
+		return frappe._dict({
+			"product_info": {},
+			"cart_settings": cart_settings
+		})
+
+	cart_quotation = frappe._dict()
+	if not skip_quotation_creation:
+		cart_quotation = _get_cart_quotation()
+
+	selling_price_list = cart_quotation.get("selling_price_list") if cart_quotation else _set_price_list(cart_settings, None)
+
+	price = {}
+	if cart_settings.show_price:
+		is_guest = frappe.session.user == "Guest"
+		# Show Price if logged in.
+		# If not logged in, check if price is hidden for guest.
+		if not is_guest or not cart_settings.hide_price_for_guest:
+			price = get_price(
+				item_code,
+				selling_price_list,
+				cart_settings.default_customer_group,
+				cart_settings.company
+			)
+
+	stock_status = None
+
+	if cart_settings.show_stock_availability:
+		on_backorder = frappe.get_cached_value("Website Item", {"item_code": item_code}, "on_backorder")
+		if on_backorder:
+			stock_status = frappe._dict({"on_backorder": True})
+		else:
+			stock_status = get_web_item_qty_in_stock(item_code, "website_warehouse")
+
+	product_info = {
+		"price": price,
+		"qty": 0,
+		"uom": frappe.db.get_value("Item", item_code, "stock_uom"),
+		"sales_uom": frappe.db.get_value("Item", item_code, "sales_uom")
+	}
+
+	if stock_status:
+		if stock_status.on_backorder:
+			product_info["on_backorder"] = True
+		else:
+			product_info["stock_qty"] = stock_status.stock_qty
+			product_info["in_stock"] = stock_status.in_stock if stock_status.is_stock_item else get_non_stock_item_status(item_code, "website_warehouse")
+			product_info["show_stock_qty"] = show_quantity_in_website()
+
+	if product_info["price"]:
+		if frappe.session.user != "Guest":
+			item = cart_quotation.get({"item_code": item_code}) if cart_quotation else None
+			if item:
+				product_info["qty"] = item[0].qty
+
+	return frappe._dict({
+		"product_info": product_info,
+		"cart_settings": cart_settings
+	})
+
+def set_product_info_for_website(item):
+	"""set product price uom for website"""
+	product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get("product_info")
+
+	if product_info:
+		item.update(product_info)
+		item["stock_uom"] = product_info.get("uom")
+		item["sales_uom"] = product_info.get("sales_uom")
+		if product_info.get("price"):
+			item["price_stock_uom"] = product_info.get("price").get("formatted_price")
+			item["price_sales_uom"] = product_info.get("price").get("formatted_price_sales_uom")
+		else:
+			item["price_stock_uom"] = ""
+			item["price_sales_uom"] = ""
diff --git a/erpnext/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
similarity index 77%
rename from erpnext/shopping_cart/test_shopping_cart.py
rename to erpnext/e_commerce/shopping_cart/test_shopping_cart.py
index 60c220a..8519e68 100644
--- a/erpnext/shopping_cart/test_shopping_cart.py
+++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py
@@ -8,8 +8,14 @@
 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.tests.utils import create_test_contact_and_address
+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_cart_quotation,
+	get_party,
+	update_cart,
+)
+from erpnext.tests.utils import change_settings, create_test_contact_and_address
 
 # test_dependencies = ['Payment Terms Template']
 
@@ -27,8 +33,14 @@
 		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.db.rollback()
 		frappe.set_user("Administrator")
 		self.disable_shopping_cart()
 
@@ -123,6 +135,43 @@
 
 		self.remove_test_quotation(quotation)
 
+	@change_settings("E Commerce Settings",{
+		"company": "_Test Company",
+		"enabled": 1,
+		"default_customer_group": "_Test Customer Group",
+		"price_list": "_Test Price List India",
+		"show_price": 1
+	})
+	def test_add_item_variant_without_web_item_to_cart(self):
+		"Test adding Variants having no Website Items in cart via Template Web Item."
+		from erpnext.controllers.item_variant import create_variant
+		from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
+		from erpnext.stock.doctype.item.test_item import make_item
+
+		template_item = make_item("Test-Tshirt-Temp", {
+			"has_variant": 1,
+			"variant_based_on": "Item Attribute",
+			"attributes": [
+				{"attribute": "Test Size"},
+				{"attribute": "Test Colour"}
+			]
+		})
+		variant = create_variant("Test-Tshirt-Temp", {
+			"Test Size": "Small", "Test Colour": "Red"
+		})
+		variant.save()
+		make_website_item(template_item) # publish template not variant
+
+		update_cart("Test-Tshirt-Temp-S-R", 1)
+
+		cart = get_cart_quotation() # test if cart page gets data without errors
+		doc = cart.get("doc")
+
+		self.assertEqual(doc.get("items")[0].item_name, "Test-Tshirt-Temp-S-R")
+
+		# test if items are rendered without error
+		frappe.render_template("templates/includes/cart/cart_items.html", cart)
+
 	def create_tax_rule(self):
 		tax_rule = frappe.get_test_records("Tax Rule")[0]
 		try:
@@ -166,7 +215,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,
@@ -196,7 +245,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 84%
rename from erpnext/shopping_cart/utils.py
rename to erpnext/e_commerce/shopping_cart/utils.py
index 5f0c792..e9745a4 100644
--- a/erpnext/shopping_cart/utils.py
+++ b/erpnext/e_commerce/shopping_cart/utils.py
@@ -1,10 +1,8 @@
-# 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
 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():
@@ -23,7 +21,7 @@
 		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 will try to fetch existing cart quotation
 		# or create one if non existent (and create a customer too)
diff --git a/erpnext/portal/product_configurator/__init__.py b/erpnext/e_commerce/variant_selector/__init__.py
similarity index 100%
rename from erpnext/portal/product_configurator/__init__.py
rename to erpnext/e_commerce/variant_selector/__init__.py
diff --git a/erpnext/portal/product_configurator/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py
similarity index 82%
rename from erpnext/portal/product_configurator/item_variants_cache.py
rename to erpnext/e_commerce/variant_selector/item_variants_cache.py
index 636ae8d..bb6b3ef 100644
--- a/erpnext/portal/product_configurator/item_variants_cache.py
+++ b/erpnext/e_commerce/variant_selector/item_variants_cache.py
@@ -44,7 +44,7 @@
 		val = frappe.cache().get_value('ordered_attribute_values_map')
 		if val: return val
 
-		all_attribute_values = frappe.db.get_all('Item Attribute Value',
+		all_attribute_values = frappe.get_all('Item Attribute Value',
 			['attribute_value', 'idx', 'parent'], order_by='idx asc')
 
 		ordered_attribute_values_map = frappe._dict({})
@@ -57,22 +57,35 @@
 	def build_cache(self):
 		parent_item_code = self.item_code
 
-		attributes = [a.attribute for a in frappe.db.get_all('Item Variant Attribute',
-			{'parent': parent_item_code}, ['attribute'], order_by='idx asc')
+		attributes = [
+			a.attribute for a in frappe.get_all(
+				'Item Variant Attribute',
+				{'parent': parent_item_code},
+				['attribute'],
+				order_by='idx asc'
+			)
 		]
 
-		item_variants_data = frappe.db.get_all('Item Variant Attribute',
-			{'variant_of': parent_item_code}, ['parent', 'attribute', 'attribute_value'],
+		# join with Website Item
+		item_variants_data = frappe.get_all(
+			'Item Variant Attribute',
+			{'variant_of': parent_item_code},
+			['parent', 'attribute', 'attribute_value'],
 			order_by='name',
 			as_list=1
 		)
 
-		disabled_items = set([i.name for i in frappe.db.get_all('Item', {'disabled': 1})])
+		disabled_items = set(
+			[i.name for i in frappe.db.get_all('Item', {'disabled': 1})]
+		)
 
-		attribute_value_item_map = frappe._dict({})
-		item_attribute_value_map = frappe._dict({})
+		attribute_value_item_map = frappe._dict()
+		item_attribute_value_map = frappe._dict()
 
+		# dont consider variants that are disabled
+		# pull all other variants
 		item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items]
+
 		for row in item_variants_data:
 			item_code, attribute, attribute_value = row
 			# (attr, value) => [item1, item2]
diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py
new file mode 100644
index 0000000..b83961e
--- /dev/null
+++ b/erpnext/e_commerce/variant_selector/test_variant_selector.py
@@ -0,0 +1,117 @@
+import frappe
+
+from erpnext.controllers.item_variant import create_variant
+from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
+	setup_e_commerce_settings,
+)
+from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
+from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.tests.utils import ERPNextTestCase
+
+test_dependencies = ["Item"]
+
+class TestVariantSelector(ERPNextTestCase):
+
+	@classmethod
+	def setUpClass(cls):
+		template_item = make_item("Test-Tshirt-Temp", {
+			"has_variant": 1,
+			"variant_based_on": "Item Attribute",
+			"attributes": [
+				{"attribute": "Test Size"},
+				{"attribute": "Test Colour"}
+			]
+		})
+
+		# create L-R, L-G, M-R, M-G and S-R
+		for size in ("Large", "Medium",):
+			for colour in ("Red", "Green",):
+				variant = create_variant("Test-Tshirt-Temp", {
+					"Test Size": size, "Test Colour": colour
+				})
+				variant.save()
+
+		variant = create_variant("Test-Tshirt-Temp", {
+			"Test Size": "Small", "Test Colour": "Red"
+		})
+		variant.save()
+
+		make_website_item(template_item) # publish template not variants
+
+	def test_item_attributes(self):
+		"""
+			Test if the right attributes are fetched in the popup.
+			(Attributes must only come from active items)
+
+			Attribute selection must not be linked to Website Items.
+		"""
+		from erpnext.e_commerce.variant_selector.utils import get_attributes_and_values
+
+		attr_data = get_attributes_and_values("Test-Tshirt-Temp")
+
+		self.assertEqual(attr_data[0]["attribute"], "Test Size")
+		self.assertEqual(attr_data[1]["attribute"], "Test Colour")
+		self.assertEqual(len(attr_data[0]["values"]), 3) # ['Small', 'Medium', 'Large']
+		self.assertEqual(len(attr_data[1]["values"]), 2) # ['Red', 'Green']
+
+		# disable small red tshirt, now there are no small tshirts.
+		# but there are some red tshirts
+		small_variant = frappe.get_doc("Item", "Test-Tshirt-Temp-S-R")
+		small_variant.disabled = 1
+		small_variant.save() # trigger cache rebuild
+
+		attr_data = get_attributes_and_values("Test-Tshirt-Temp")
+
+		# Only L and M attribute values must be fetched since S is disabled
+		self.assertEqual(len(attr_data[0]["values"]), 2)  # ['Medium', 'Large']
+
+		# teardown
+		small_variant.disabled = 0
+		small_variant.save()
+
+	def test_next_item_variant_values(self):
+		"""
+			Test if on selecting an attribute value, the next possible values
+			are filtered accordingly.
+			Values that dont apply should not be fetched.
+			E.g.
+			There is a ** Small-Red ** Tshirt. No other colour in this size.
+			On selecting ** Small **, only ** Red ** should be selectable next.
+		"""
+		next_values = get_next_attribute_and_values("Test-Tshirt-Temp", selected_attributes={"Test Size": "Small"})
+		next_colours = next_values["valid_options_for_attributes"]["Test Colour"]
+		filtered_items = next_values["filtered_items"]
+
+		self.assertEqual(len(next_colours), 1)
+		self.assertEqual(next_colours.pop(), "Red")
+		self.assertEqual(len(filtered_items), 1)
+		self.assertEqual(filtered_items.pop(), "Test-Tshirt-Temp-S-R")
+
+	def test_exact_match_with_price(self):
+		"""
+			Test price fetching and matching of variant without Website Item
+		"""
+		from erpnext.e_commerce.doctype.website_item.test_website_item import make_web_item_price
+
+		frappe.set_user("Administrator")
+		setup_e_commerce_settings({
+			"company": "_Test Company",
+			"enabled": 1,
+			"default_customer_group": "_Test Customer Group",
+			"price_list": "_Test Price List India",
+			"show_price": 1
+		})
+
+		make_web_item_price(item_code="Test-Tshirt-Temp-S-R", price_list_rate=100)
+		next_values = get_next_attribute_and_values(
+			"Test-Tshirt-Temp",
+			selected_attributes={"Test Size": "Small", "Test Colour": "Red"}
+		)
+		print(">>>>", next_values)
+		price_info = next_values["product_info"]["price"]
+
+		self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
+		self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
+		self.assertEqual(price_info["price_list_rate"], 100.0)
+		self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00")
\ No newline at end of file
diff --git a/erpnext/e_commerce/variant_selector/utils.py b/erpnext/e_commerce/variant_selector/utils.py
new file mode 100644
index 0000000..3380273
--- /dev/null
+++ b/erpnext/e_commerce/variant_selector/utils.py
@@ -0,0 +1,218 @@
+import frappe
+from frappe.utils import cint
+
+from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
+	get_shopping_cart_settings,
+)
+from erpnext.e_commerce.shopping_cart.cart import _set_price_list
+from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager
+from erpnext.utilities.product import get_price
+
+
+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)]) # nosemgrep
+		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:
+		cart_settings = get_shopping_cart_settings()
+		product_info = get_item_variant_price_dict(exact_match[0], cart_settings)
+
+		if product_info:
+			product_info["allow_items_not_in_stock"] = cint(cart_settings.allow_items_not_in_stock)
+	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
+
+def get_item_variant_price_dict(item_code, cart_settings):
+	if cart_settings.enabled and cart_settings.show_price:
+		is_guest = frappe.session.user == "Guest"
+		# Show Price if logged in.
+		# If not logged in, check if price is hidden for guest.
+		if not is_guest or not cart_settings.hide_price_for_guest:
+			price_list = _set_price_list(cart_settings, None)
+			price = get_price(
+				item_code,
+				price_list,
+				cart_settings.default_customer_group,
+				cart_settings.company
+			)
+			return {"price": price}
+
+	return None
+
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 81%
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..07952f0 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
@@ -23,11 +23,10 @@
 		{%- for index in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] -%}
 		{%- set item = values['card_' + index + '_item'] -%}
 			{%- if item -%}
-				{%- set item = frappe.get_doc("Item", item) -%}
+				{%- set web_item = frappe.get_doc("Website Item", item) -%}
 				{{ item_card(
-					item.item_name, item.image, item.route, item.description,
-					None, item.item_group, values['card_' + index + '_featured'],
-					True, "Center"
+					web_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 84%
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..ad9e2a7 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",
@@ -40,8 +37,8 @@
   {
    "fieldname": "card_1_item",
    "fieldtype": "Link",
-   "label": "Item",
-   "options": "Item",
+   "label": "Website Item",
+   "options": "Website Item",
    "reqd": 0
   },
   {
@@ -59,8 +56,8 @@
   {
    "fieldname": "card_2_item",
    "fieldtype": "Link",
-   "label": "Item",
-   "options": "Item",
+   "label": "Website Item",
+   "options": "Website Item",
    "reqd": 0
   },
   {
@@ -79,8 +76,8 @@
   {
    "fieldname": "card_3_item",
    "fieldtype": "Link",
-   "label": "Item",
-   "options": "Item",
+   "label": "Website Item",
+   "options": "Website Item",
    "reqd": 0
   },
   {
@@ -98,8 +95,8 @@
   {
    "fieldname": "card_4_item",
    "fieldtype": "Link",
-   "label": "Item",
-   "options": "Item",
+   "label": "Website Item",
+   "options": "Website Item",
    "reqd": 0
   },
   {
@@ -117,8 +114,8 @@
   {
    "fieldname": "card_5_item",
    "fieldtype": "Link",
-   "label": "Item",
-   "options": "Item",
+   "label": "Website Item",
+   "options": "Website Item",
    "reqd": 0
   },
   {
@@ -136,8 +133,8 @@
   {
    "fieldname": "card_6_item",
    "fieldtype": "Link",
-   "label": "Item",
-   "options": "Item",
+   "label": "Website Item",
+   "options": "Website Item",
    "reqd": 0
   },
   {
@@ -155,8 +152,8 @@
   {
    "fieldname": "card_7_item",
    "fieldtype": "Link",
-   "label": "Item",
-   "options": "Item",
+   "label": "Website Item",
+   "options": "Website Item",
    "reqd": 0
   },
   {
@@ -174,8 +171,8 @@
   {
    "fieldname": "card_8_item",
    "fieldtype": "Link",
-   "label": "Item",
-   "options": "Item",
+   "label": "Website Item",
+   "options": "Website Item",
    "reqd": 0
   },
   {
@@ -193,8 +190,8 @@
   {
    "fieldname": "card_9_item",
    "fieldtype": "Link",
-   "label": "Item",
-   "options": "Item",
+   "label": "Website Item",
+   "options": "Website Item",
    "reqd": 0
   },
   {
@@ -212,8 +209,8 @@
   {
    "fieldname": "card_10_item",
    "fieldtype": "Link",
-   "label": "Item",
-   "options": "Item",
+   "label": "Website Item",
+   "options": "Website Item",
    "reqd": 0
   },
   {
@@ -231,8 +228,8 @@
   {
    "fieldname": "card_11_item",
    "fieldtype": "Link",
-   "label": "Item",
-   "options": "Item",
+   "label": "Website Item",
+   "options": "Website Item",
    "reqd": 0
   },
   {
@@ -250,8 +247,8 @@
   {
    "fieldname": "card_12_item",
    "fieldtype": "Link",
-   "label": "Item",
-   "options": "Item",
+   "label": "Website Item",
+   "options": "Website Item",
    "reqd": 0
   },
   {
@@ -262,9 +259,9 @@
   }
  ],
  "idx": 0,
- "modified": "2020-11-19 18:48:52.633045",
+ "modified": "2021-12-21 14:44:59.821335",
  "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 81%
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
index 06b76af..6d75a8b 100644
--- 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
@@ -6,8 +6,15 @@
 }) -%}
 <div class="card h-100">
 	{% if image %}
-	<img class="card-img-top" src="{{ image }}" alt="{{ title }}">
+	<img class="card-img-top" src="{{ image }}" alt="{{ title }}" style="max-height: 200px;">
+	{% else %}
+	<div class="placeholder-div" style="max-height: 200px;">
+		<span class="placeholder">
+			{{ frappe.utils.get_abbr(title or '') }}
+		</span>
+	</div>
 	{% endif %}
+
 	<div class="card-body text-center text-muted small">
 		{{ title or '' }}
 	</div>
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/education/doctype/program_enrollment/program_enrollment.py b/erpnext/education/doctype/program_enrollment/program_enrollment.py
index a23d492..4d0f3a9 100644
--- a/erpnext/education/doctype/program_enrollment/program_enrollment.py
+++ b/erpnext/education/doctype/program_enrollment/program_enrollment.py
@@ -6,6 +6,7 @@
 from frappe import _, msgprint
 from frappe.desk.reportview import get_match_cond
 from frappe.model.document import Document
+from frappe.query_builder.functions import Min
 from frappe.utils import comma_and, get_link_to_form, getdate
 
 
@@ -60,8 +61,15 @@
 			frappe.throw(_("Student is already enrolled."))
 
 	def update_student_joining_date(self):
-		date = frappe.db.sql("select min(enrollment_date) from `tabProgram Enrollment` where student= %s", self.student)
-		frappe.db.set_value("Student", self.student, "joining_date", date)
+		table = frappe.qb.DocType('Program Enrollment')
+		date = (
+			frappe.qb.from_(table)
+				.select(Min(table.enrollment_date).as_('enrollment_date'))
+				.where(table.student == self.student)
+		).run(as_dict=True)
+
+		if date:
+			frappe.db.set_value("Student", self.student, "joining_date", date[0].enrollment_date)
 
 	def make_fee_records(self):
 		from erpnext.education.api import get_fee_components
diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py
index 03c1a1a..29bc36f 100644
--- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py
+++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py
@@ -149,7 +149,6 @@
 	item.description = amazon_item_json.Product.AttributeSets.ItemAttributes.Title
 	item.brand = new_brand
 	item.manufacturer = new_manufacturer
-	item.web_long_description = amazon_item_json.Product.AttributeSets.ItemAttributes.Title
 
 	item.image = amazon_item_json.Product.AttributeSets.ItemAttributes.SmallImage.URL
 
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 04f5793..0e29038 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -51,15 +51,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"]
@@ -73,7 +73,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 = {
@@ -237,10 +237,7 @@
 		]
 	},
 	"Sales Taxes and Charges Template": {
-		"on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings"
-	},
-	"Website Settings": {
-		"validate": "erpnext.portal.doctype.products_settings.products_settings.home_page_is_products"
+		"on_update": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.validate_cart_settings"
 	},
 	"Tax Category": {
 		"validate": "erpnext.regional.india.utils.validate_tax_category"
diff --git a/erpnext/hr/doctype/employee/employee_reminders.py b/erpnext/hr/doctype/employee/employee_reminders.py
index 559bd39..0bb6637 100644
--- a/erpnext/hr/doctype/employee/employee_reminders.py
+++ b/erpnext/hr/doctype/employee/employee_reminders.py
@@ -20,6 +20,7 @@
 
 	send_advance_holiday_reminders("Weekly")
 
+
 def send_reminders_in_advance_monthly():
 	to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders"))
 	frequency = frappe.db.get_single_value("HR Settings", "frequency")
@@ -28,6 +29,7 @@
 
 	send_advance_holiday_reminders("Monthly")
 
+
 def send_advance_holiday_reminders(frequency):
 	"""Send Holiday Reminders in Advance to Employees
 	`frequency` (str): 'Weekly' or 'Monthly'
@@ -42,7 +44,7 @@
 	else:
 		return
 
-	employees = frappe.db.get_all('Employee', pluck='name')
+	employees = frappe.db.get_all('Employee', filters={'status': 'Active'}, pluck='name')
 	for employee in employees:
 		holidays = get_holidays_for_employee(
 			employee,
@@ -51,10 +53,13 @@
 			raise_exception=False
 		)
 
-		if not (holidays is None):
-			send_holidays_reminder_in_advance(employee, holidays)
+		send_holidays_reminder_in_advance(employee, holidays)
+
 
 def send_holidays_reminder_in_advance(employee, holidays):
+	if not holidays:
+		return
+
 	employee_doc = frappe.get_doc('Employee', employee)
 	employee_email = get_employee_email(employee_doc)
 	frequency = frappe.db.get_single_value("HR Settings", "frequency")
@@ -101,6 +106,7 @@
 				reminder_text, message = get_birthday_reminder_text_and_message(others)
 				send_birthday_reminder(person_email, reminder_text, others, message)
 
+
 def get_birthday_reminder_text_and_message(birthday_persons):
 	if len(birthday_persons) == 1:
 		birthday_person_text = birthday_persons[0]['name']
@@ -116,6 +122,7 @@
 
 	return reminder_text, message
 
+
 def send_birthday_reminder(recipients, reminder_text, birthday_persons, message):
 	frappe.sendmail(
 		recipients=recipients,
@@ -129,10 +136,12 @@
 		header=_("Birthday Reminder 🎂")
 	)
 
+
 def get_employees_who_are_born_today():
 	"""Get all employee born today & group them based on their company"""
 	return get_employees_having_an_event_today("birthday")
 
+
 def get_employees_having_an_event_today(event_type):
 	"""Get all employee who have `event_type` today
 	& group them based on their company. `event_type`
@@ -210,13 +219,14 @@
 				reminder_text, message = get_work_anniversary_reminder_text_and_message(others)
 				send_work_anniversary_reminder(person_email, reminder_text, others, message)
 
+
 def get_work_anniversary_reminder_text_and_message(anniversary_persons):
 	if len(anniversary_persons) == 1:
 		anniversary_person = anniversary_persons[0]['name']
 		persons_name = anniversary_person
 		# Number of years completed at the company
 		completed_years = getdate().year - anniversary_persons[0]['date_of_joining'].year
-		anniversary_person += f" completed {completed_years} years"
+		anniversary_person += f" completed {completed_years} year(s)"
 	else:
 		person_names_with_years = []
 		names = []
@@ -225,7 +235,7 @@
 			names.append(person_text)
 			# Number of years completed at the company
 			completed_years = getdate().year - person['date_of_joining'].year
-			person_text += f" completed {completed_years} years"
+			person_text += f" completed {completed_years} year(s)"
 			person_names_with_years.append(person_text)
 
 		# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
@@ -239,6 +249,7 @@
 
 	return reminder_text, message
 
+
 def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message):
 	frappe.sendmail(
 		recipients=recipients,
@@ -249,5 +260,5 @@
 			anniversary_persons=anniversary_persons,
 			message=message,
 		),
-		header=_("🎊️🎊️ Work Anniversary Reminder 🎊️🎊️")
+		header=_("Work Anniversary Reminder")
 	)
diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py
index 8a2da08..67cbea6 100644
--- a/erpnext/hr/doctype/employee/test_employee.py
+++ b/erpnext/hr/doctype/employee/test_employee.py
@@ -36,7 +36,7 @@
 		employee_doc.reload()
 
 		make_holiday_list()
-		frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List")
+		frappe.db.set_value("Company", employee_doc.company, "default_holiday_list", "Salary Slip Test Holiday List")
 
 		frappe.db.sql("""delete from `tabSalary Structure` where name='Test Inactive Employee Salary Slip'""")
 		salary_structure = make_salary_structure("Test Inactive Employee Salary Slip", "Monthly",
diff --git a/erpnext/hr/doctype/employee/test_employee_reminders.py b/erpnext/hr/doctype/employee/test_employee_reminders.py
index 52c0098..a4097ab 100644
--- a/erpnext/hr/doctype/employee/test_employee_reminders.py
+++ b/erpnext/hr/doctype/employee/test_employee_reminders.py
@@ -5,10 +5,12 @@
 from datetime import timedelta
 
 import frappe
-from frappe.utils import getdate
+from frappe.utils import add_months, getdate
 
+from erpnext.hr.doctype.employee.employee_reminders import send_holidays_reminder_in_advance
 from erpnext.hr.doctype.employee.test_employee import make_employee
 from erpnext.hr.doctype.hr_settings.hr_settings import set_proceed_with_frequency_change
+from erpnext.hr.utils import get_holidays_for_employee
 
 
 class TestEmployeeReminders(unittest.TestCase):
@@ -46,6 +48,24 @@
 		cls.test_employee = test_employee
 		cls.test_holiday_dates = test_holiday_dates
 
+		# Employee without holidays in this month/week
+		test_employee_2 = make_employee('test@empwithoutholiday.io', company="_Test Company")
+		test_employee_2 = frappe.get_doc('Employee', test_employee_2)
+
+		test_holiday_list = make_holiday_list(
+			'TestHolidayRemindersList2',
+			holiday_dates=[
+				{'holiday_date': add_months(getdate(), 1), 'description': 'test holiday1'},
+			],
+			from_date=add_months(getdate(), -2),
+			to_date=add_months(getdate(), 2)
+		)
+		test_employee_2.holiday_list = test_holiday_list.name
+		test_employee_2.save()
+
+		cls.test_employee_2 = test_employee_2
+		cls.holiday_list_2 = test_holiday_list
+
 	@classmethod
 	def get_test_holiday_dates(cls):
 		today_date = getdate()
@@ -61,6 +81,7 @@
 	def setUp(self):
 		# Clear Email Queue
 		frappe.db.sql("delete from `tabEmail Queue`")
+		frappe.db.sql("delete from `tabEmail Queue Recipient`")
 
 	def test_is_holiday(self):
 		from erpnext.hr.doctype.employee.employee import is_holiday
@@ -103,11 +124,10 @@
 		self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message)
 
 	def test_work_anniversary_reminders(self):
-		employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
-		employee.date_of_joining = "1998" + frappe.utils.nowdate()[4:]
-		employee.company_email = "test@example.com"
-		employee.company = "_Test Company"
-		employee.save()
+		make_employee("test_work_anniversary@gmail.com",
+			date_of_joining="1998" + frappe.utils.nowdate()[4:],
+			company="_Test Company",
+		)
 
 		from erpnext.hr.doctype.employee.employee_reminders import (
 			get_employees_having_an_event_today,
@@ -115,7 +135,12 @@
 		)
 
 		employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary')
-		self.assertTrue(employees_having_work_anniversary.get("_Test Company"))
+		employees = employees_having_work_anniversary.get("_Test Company") or []
+		user_ids = []
+		for entry in employees:
+			user_ids.append(entry.user_id)
+
+		self.assertTrue("test_work_anniversary@gmail.com" in user_ids)
 
 		hr_settings = frappe.get_doc("HR Settings", "HR Settings")
 		hr_settings.send_work_anniversary_reminders = 1
@@ -126,16 +151,24 @@
 		email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
 		self.assertTrue("Subject: Work Anniversary Reminder" in email_queue[0].message)
 
-	def test_send_holidays_reminder_in_advance(self):
-		from erpnext.hr.doctype.employee.employee_reminders import send_holidays_reminder_in_advance
-		from erpnext.hr.utils import get_holidays_for_employee
+	def test_work_anniversary_reminder_not_sent_for_0_years(self):
+		make_employee("test_work_anniversary_2@gmail.com",
+			date_of_joining=getdate(),
+			company="_Test Company",
+		)
 
-		# Get HR settings and enable advance holiday reminders
-		hr_settings = frappe.get_doc("HR Settings", "HR Settings")
-		hr_settings.send_holiday_reminders = 1
-		set_proceed_with_frequency_change()
-		hr_settings.frequency = 'Weekly'
-		hr_settings.save()
+		from erpnext.hr.doctype.employee.employee_reminders import get_employees_having_an_event_today
+
+		employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary')
+		employees = employees_having_work_anniversary.get("_Test Company") or []
+		user_ids = []
+		for entry in employees:
+			user_ids.append(entry.user_id)
+
+		self.assertTrue("test_work_anniversary_2@gmail.com" not in user_ids)
+
+	def test_send_holidays_reminder_in_advance(self):
+		setup_hr_settings('Weekly')
 
 		holidays = get_holidays_for_employee(
 					self.test_employee.get('name'),
@@ -151,32 +184,80 @@
 
 		email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
 		self.assertEqual(len(email_queue), 1)
+		self.assertTrue("Holidays this Week." in email_queue[0].message)
 
 	def test_advance_holiday_reminders_monthly(self):
 		from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_monthly
 
-		# Get HR settings and enable advance holiday reminders
-		hr_settings = frappe.get_doc("HR Settings", "HR Settings")
-		hr_settings.send_holiday_reminders = 1
-		set_proceed_with_frequency_change()
-		hr_settings.frequency = 'Monthly'
-		hr_settings.save()
+		setup_hr_settings('Monthly')
+
+		# disable emp 2, set same holiday list
+		frappe.db.set_value('Employee', self.test_employee_2.name, {
+			'status': 'Left',
+			'holiday_list': self.test_employee.holiday_list
+		})
 
 		send_reminders_in_advance_monthly()
-
 		email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
 		self.assertTrue(len(email_queue) > 0)
 
+		# even though emp 2 has holiday, non-active employees should not be recipients
+		recipients = frappe.db.get_all('Email Queue Recipient', pluck='recipient')
+		self.assertTrue(self.test_employee_2.user_id not in recipients)
+
+		# teardown: enable emp 2
+		frappe.db.set_value('Employee', self.test_employee_2.name, {
+			'status': 'Active',
+			'holiday_list': self.holiday_list_2.name
+		})
+
 	def test_advance_holiday_reminders_weekly(self):
 		from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_weekly
 
-		# Get HR settings and enable advance holiday reminders
-		hr_settings = frappe.get_doc("HR Settings", "HR Settings")
-		hr_settings.send_holiday_reminders = 1
-		hr_settings.frequency = 'Weekly'
-		hr_settings.save()
+		setup_hr_settings('Weekly')
+
+		# disable emp 2, set same holiday list
+		frappe.db.set_value('Employee', self.test_employee_2.name, {
+			'status': 'Left',
+			'holiday_list': self.test_employee.holiday_list
+		})
 
 		send_reminders_in_advance_weekly()
-
 		email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
 		self.assertTrue(len(email_queue) > 0)
+
+		# even though emp 2 has holiday, non-active employees should not be recipients
+		recipients = frappe.db.get_all('Email Queue Recipient', pluck='recipient')
+		self.assertTrue(self.test_employee_2.user_id not in recipients)
+
+		# teardown: enable emp 2
+		frappe.db.set_value('Employee', self.test_employee_2.name, {
+			'status': 'Active',
+			'holiday_list': self.holiday_list_2.name
+		})
+
+	def test_reminder_not_sent_if_no_holdays(self):
+		setup_hr_settings('Monthly')
+
+		# reminder not sent if there are no holidays
+		holidays = get_holidays_for_employee(
+			self.test_employee_2.get('name'),
+			getdate(), getdate() + timedelta(days=3),
+			only_non_weekly=True,
+			raise_exception=False
+		)
+		send_holidays_reminder_in_advance(
+			self.test_employee_2.get('name'),
+			holidays
+		)
+		email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
+		self.assertEqual(len(email_queue), 0)
+
+
+def setup_hr_settings(frequency=None):
+	# Get HR settings and enable advance holiday reminders
+	hr_settings = frappe.get_doc("HR Settings", "HR Settings")
+	hr_settings.send_holiday_reminders = 1
+	set_proceed_with_frequency_change()
+	hr_settings.frequency = frequency or 'Weekly'
+	hr_settings.save()
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 75e99f8..6b85927 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -75,10 +75,8 @@
 			frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec
 
 		frappe.set_user("Administrator")
-
-	@classmethod
-	def setUpClass(cls):
 		set_leave_approver()
+
 		frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'")
 
 	def tearDown(self):
@@ -134,10 +132,11 @@
 		make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
 
 		holiday_list = make_holiday_list()
-		frappe.db.set_value("Company", "_Test Company", "default_holiday_list", holiday_list)
+		employee = get_employee()
+		frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list)
 		first_sunday = get_first_sunday(holiday_list)
 
-		leave_application = make_leave_application("_T-Employee-00001", first_sunday, add_days(first_sunday, 3), leave_type.name)
+		leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name)
 		leave_application.reload()
 		self.assertEqual(leave_application.total_leave_days, 4)
 		self.assertEqual(frappe.db.count('Attendance', {'leave_application': leave_application.name}), 4)
@@ -157,25 +156,28 @@
 		make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
 
 		holiday_list = make_holiday_list()
-		frappe.db.set_value("Company", "_Test Company", "default_holiday_list", holiday_list)
+		employee = get_employee()
+		frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list)
 		first_sunday = get_first_sunday(holiday_list)
 
 		# already marked attendance on a holiday should be deleted in this case
 		config = {
 			"doctype": "Attendance",
-			"employee": "_T-Employee-00001",
+			"employee": employee.name,
 			"status": "Present"
 		}
 		attendance_on_holiday = frappe.get_doc(config)
 		attendance_on_holiday.attendance_date = first_sunday
+		attendance_on_holiday.flags.ignore_validate = True
 		attendance_on_holiday.save()
 
 		# already marked attendance on a non-holiday should be updated
 		attendance = frappe.get_doc(config)
 		attendance.attendance_date = add_days(first_sunday, 3)
+		attendance.flags.ignore_validate = True
 		attendance.save()
 
-		leave_application = make_leave_application("_T-Employee-00001", first_sunday, add_days(first_sunday, 3), leave_type.name)
+		leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name)
 		leave_application.reload()
 		# holiday should be excluded while marking attendance
 		self.assertEqual(leave_application.total_leave_days, 3)
@@ -325,7 +327,7 @@
 		employee = get_employee()
 
 		default_holiday_list = make_holiday_list()
-		frappe.db.set_value("Company", "_Test Company", "default_holiday_list", default_holiday_list)
+		frappe.db.set_value("Company", employee.company, "default_holiday_list", default_holiday_list)
 		first_sunday = get_first_sunday(default_holiday_list)
 
 		optional_leave_date = add_days(first_sunday, 1)
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index fc3b971..8a7634e 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -93,7 +93,7 @@
 			});
 		}
 
-		if(frm.doc.docstatus!=0) {
+		if(frm.doc.docstatus==1) {
 			frm.add_custom_button(__("Work Order"), function() {
 				frm.trigger("make_work_order");
 			}, __("Create"));
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 21a126b..276e708 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -385,6 +385,61 @@
 		# lowest most level of subassembly should be first
 		self.assertIn("SuperSecret", plan.sub_assembly_items[0].production_item)
 
+	def test_multiple_work_order_for_production_plan_item(self):
+		def create_work_order(item, pln, qty):
+			# Get Production Items
+			items_data = pln.get_production_items()
+
+			# Update qty
+			items_data[(item, None, None)]["qty"] = qty
+
+			# Create and Submit Work Order for each item in items_data
+			for key, item in items_data.items():
+				if pln.sub_assembly_items:
+					item['use_multi_level_bom'] = 0
+
+				wo_name = pln.create_work_order(item)
+				wo_doc = frappe.get_doc("Work Order", wo_name)
+				wo_doc.update({
+					'wip_warehouse': 'Work In Progress - _TC',
+					'fg_warehouse': 'Finished Goods - _TC'
+				})
+				wo_doc.submit()
+				wo_list.append(wo_name)
+
+		item = "Test Production Item 1"
+		raw_materials = ["Raw Material Item 1", "Raw Material Item 2"]
+
+		# Create BOM
+		bom = make_bom(item=item, raw_materials=raw_materials)
+
+		# Create Production Plan
+		pln = create_production_plan(item_code=bom.item, planned_qty=10)
+
+		# All the created Work Orders
+		wo_list = []
+
+		# Create and Submit 1st Work Order for 5 qty
+		create_work_order(item, pln, 5)
+		pln.reload()
+		self.assertEqual(pln.po_items[0].ordered_qty, 5)
+
+		# Create and Submit 2nd Work Order for 3 qty
+		create_work_order(item, pln, 3)
+		pln.reload()
+		self.assertEqual(pln.po_items[0].ordered_qty, 8)
+
+		# Cancel 1st Work Order
+		wo1 = frappe.get_doc("Work Order", wo_list[0])
+		wo1.cancel()
+		pln.reload()
+		self.assertEqual(pln.po_items[0].ordered_qty, 3)
+
+		# Cancel 2nd Work Order
+		wo2 = frappe.get_doc("Work Order", wo_list[1])
+		wo2.cancel()
+		pln.reload()
+		self.assertEqual(pln.po_items[0].ordered_qty, 0)
 
 def create_production_plan(**args):
 	args = frappe._dict(args)
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 03e0910..a86edfa 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -449,7 +449,13 @@
 
 	def update_ordered_qty(self):
 		if self.production_plan and self.production_plan_item:
-			qty = self.qty if self.docstatus == 1 else 0
+			qty = frappe.get_value("Production Plan Item", self.production_plan_item, "ordered_qty") or 0.0
+
+			if self.docstatus == 1:
+				qty += self.qty
+			elif self.docstatus == 2:
+				qty -= self.qty
+
 			frappe.db.set_value('Production Plan Item',
 				self.production_plan_item, 'ordered_qty', qty)
 
diff --git a/erpnext/modules.txt b/erpnext/modules.txt
index e62e2bc..c5705c1 100644
--- a/erpnext/modules.txt
+++ b/erpnext/modules.txt
@@ -9,7 +9,6 @@
 Stock
 Support
 Utilities
-Shopping Cart
 Assets
 Portal
 Maintenance
@@ -21,4 +20,5 @@
 Communication
 Loan Management
 Payroll
-Telephony
\ No newline at end of file
+Telephony
+E-commerce
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 8bd2214..eace7ca 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -293,6 +293,9 @@
 erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
 erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021
 erpnext.patches.v13_0.fix_invoice_statuses
+erpnext.patches.v13_0.create_website_items #30-09-2021
+erpnext.patches.v13_0.populate_e_commerce_settings
+erpnext.patches.v13_0.make_homepage_products_website_items
 erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item
 erpnext.patches.v13_0.update_dates_in_tax_withholding_category
 erpnext.patches.v14_0.update_opportunity_currency_fields
@@ -314,6 +317,7 @@
 erpnext.patches.v14_0.delete_healthcare_doctypes
 erpnext.patches.v13_0.update_category_in_ltds_certificate
 erpnext.patches.v13_0.create_pan_field_for_india #2
+erpnext.patches.v13_0.fetch_thumbnail_in_website_items
 erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit
 erpnext.patches.v13_0.create_ksa_vat_custom_fields # 07-01-2022
 erpnext.patches.v14_0.migrate_crm_settings
@@ -343,3 +347,5 @@
 erpnext.patches.v13_0.update_sane_transfer_against
 erpnext.patches.v12_0.add_company_link_to_einvoice_settings
 erpnext.patches.v14_0.migrate_cost_center_allocations
+erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template
+erpnext.patches.v13_0.shopping_cart_to_ecommerce
diff --git a/erpnext/patches/v13_0/convert_to_website_item_in_item_card_group_template.py b/erpnext/patches/v13_0/convert_to_website_item_in_item_card_group_template.py
new file mode 100644
index 0000000..d3ee3f8
--- /dev/null
+++ b/erpnext/patches/v13_0/convert_to_website_item_in_item_card_group_template.py
@@ -0,0 +1,57 @@
+import json
+from typing import List, Union
+
+import frappe
+
+from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
+
+
+def execute():
+    """
+        Convert all Item links to Website Item link values in
+        exisitng 'Item Card Group' Web Page Block data.
+    """
+    frappe.reload_doc("e_commerce", "web_template", "item_card_group")
+
+    blocks = frappe.db.get_all(
+        "Web Page Block",
+        filters={"web_template": "Item Card Group"},
+        fields=["parent", "web_template_values", "name"]
+    )
+
+    fields = generate_fields_to_edit()
+
+    for block in blocks:
+        web_template_value = json.loads(block.get('web_template_values'))
+
+        for field in fields:
+            item = web_template_value.get(field)
+            if not item:
+                continue
+
+            if frappe.db.exists("Website Item", {"item_code": item}):
+                website_item = frappe.db.get_value("Website Item", {"item_code": item})
+            else:
+                website_item = make_new_website_item(item)
+
+            if website_item:
+                web_template_value[field] = website_item
+
+        frappe.db.set_value("Web Page Block", block.name, "web_template_values", json.dumps(web_template_value))
+
+def generate_fields_to_edit() -> List:
+    fields = []
+    for i in range(1, 13):
+        fields.append(f"card_{i}_item") # fields like 'card_1_item', etc.
+
+    return fields
+
+def make_new_website_item(item: str) -> Union[str, None]:
+    try:
+        doc = frappe.get_doc("Item", item)
+        web_item = make_website_item(doc) # returns [website_item.name, item_name]
+        return web_item[0]
+    except Exception:
+        title = f"{item}: Error while converting to Website Item "
+        frappe.log_error(title + "for Item Card Group Template" + "\n\n" + frappe.get_traceback(), title=title)
+        return None
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..da162a3
--- /dev/null
+++ b/erpnext/patches/v13_0/create_website_items.py
@@ -0,0 +1,72 @@
+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("e_commerce", "doctype", "website_item_tabbed_section")
+	frappe.reload_doc("e_commerce", "doctype", "website_offer")
+	frappe.reload_doc("e_commerce", "doctype", "recommended_items")
+	frappe.reload_doc("e_commerce", "doctype", "e_commerce_settings")
+	frappe.reload_doc("stock", "doctype", "item")
+
+	item_fields = ["item_code", "item_name", "item_group", "stock_uom", "brand", "image",
+		"has_variants", "variant_of", "description", "weightage"]
+	web_fields_to_map = ["route", "slideshow", "website_image_alt",
+		"website_warehouse", "web_long_description", "website_content", "thumbnail"]
+
+	# get all valid columns (fields) from Item master DB schema
+	item_table_fields = frappe.db.sql("desc `tabItem`", as_dict=1) # nosemgrep
+	item_table_fields = [d.get('Field') for d in item_table_fields]
+
+	# prepare fields to query from Item, check if the web field exists in Item master
+	web_query_fields = []
+	for web_field in web_fields_to_map:
+		if web_field in item_table_fields:
+			web_query_fields.append(web_field)
+			item_fields.append(web_field)
+
+	# check if the filter fields exist in Item master
+	or_filters = {}
+	for field in ["show_in_website", "show_variant_in_website"]:
+		if field in item_table_fields:
+			or_filters[field] = 1
+
+	if not web_query_fields or not or_filters:
+		# web fields to map are not present in Item master schema
+		# most likely a fresh installation that doesnt need this patch
+		return
+
+	items = frappe.db.get_all(
+		"Item",
+		fields=item_fields,
+		or_filters=or_filters
+	)
+	total_count = len(items)
+
+	for count, item in enumerate(items, start=1):
+		if frappe.db.exists("Website Item", {"item_code": item.item_code}):
+			continue
+
+		# make new 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 doctype in ("Website Item Group", "Item Website Specification"):
+			frappe.db.set_value(
+				doctype,
+				{"parenttype": "Item", "parent": item.item_code}, # filters
+				{"parenttype": "Website Item", "parent": website_item.name} # value dict
+			)
+
+		if count % 20 == 0: # commit after every 20 items
+			frappe.db.commit()
+
+		frappe.utils.update_progress_bar('Creating Website Items', count, total_count)
diff --git a/erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py b/erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py
new file mode 100644
index 0000000..32ad542
--- /dev/null
+++ b/erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py
@@ -0,0 +1,16 @@
+import frappe
+
+
+def execute():
+    if frappe.db.has_column("Item", "thumbnail"):
+        website_item = frappe.qb.DocType("Website Item").as_("wi")
+        item = frappe.qb.DocType("Item")
+
+        frappe.qb.update(website_item).inner_join(item).on(
+            website_item.item_code == item.item_code
+        ).set(
+            website_item.thumbnail, item.thumbnail
+        ).where(
+            website_item.website_image.notnull()
+            & website_item.thumbnail.isnull()
+        ).run()
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..7a7ddba
--- /dev/null
+++ b/erpnext/patches/v13_0/make_homepage_products_website_items.py
@@ -0,0 +1,15 @@
+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.flags.ignore_mandatory = True
+	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..8f9ee51
--- /dev/null
+++ b/erpnext/patches/v13_0/populate_e_commerce_settings.py
@@ -0,0 +1,62 @@
+import frappe
+from frappe.utils import cint
+
+
+def execute():
+	frappe.reload_doc("e_commerce", "doctype", "e_commerce_settings")
+	frappe.reload_doc("portal", "doctype", "website_filter_field")
+	frappe.reload_doc("portal", "doctype", "website_attribute")
+
+	products_settings_fields = [
+		"hide_variants", "products_per_page",
+		"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):
+		singles = frappe.qb.DocType("Singles")
+		query = (
+			frappe.qb.from_(singles)
+			.select(
+				singles["field"], singles.value
+			).where(
+				(singles.doctype == doctype)
+				& (singles["field"].isin(fields))
+			)
+		)
+		data = query.run(as_dict=True)
+
+		# {'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.set_value(
+			doctype,
+			{"parent": "Products Settings"},
+			{
+				"parenttype": "E Commerce Settings",
+				"parent": "E Commerce Settings"
+			},
+			update_modified=False
+		)
diff --git a/erpnext/patches/v13_0/shopping_cart_to_ecommerce.py b/erpnext/patches/v13_0/shopping_cart_to_ecommerce.py
new file mode 100644
index 0000000..35710a9
--- /dev/null
+++ b/erpnext/patches/v13_0/shopping_cart_to_ecommerce.py
@@ -0,0 +1,29 @@
+import click
+import frappe
+
+
+def execute():
+
+	frappe.delete_doc("DocType", "Shopping Cart Settings", ignore_missing=True)
+	frappe.delete_doc("DocType", "Products Settings", ignore_missing=True)
+	frappe.delete_doc("DocType", "Supplier Item Group", ignore_missing=True)
+
+	if frappe.db.get_single_value("E Commerce Settings", "enabled"):
+		notify_users()
+
+
+def notify_users():
+
+	click.secho(
+		"Shopping cart and Product settings are merged into E-commerce settings.\n"
+		"Checkout the documentation to learn more:"
+		"https://docs.erpnext.com/docs/v13/user/manual/en/e_commerce/set_up_e_commerce",
+		fg="yellow",
+	)
+
+	note = frappe.new_doc("Note")
+	note.title = "New E-Commerce Module"
+	note.public = 1
+	note.notify_on_login = 1
+	note.content = """<div class="ql-editor read-mode"><p>You are seeing this message because Shopping Cart is enabled on your site. </p><p><br></p><p>Shopping Cart Settings and Products settings are now merged into "E Commerce Settings". </p><p><br></p><p>You can learn about new and improved E-Commerce features in the official documentation.</p><ol><li data-list="bullet"><span class="ql-ui" contenteditable="false"></span><a href="https://docs.erpnext.com/docs/v13/user/manual/en/e_commerce/set_up_e_commerce" rel="noopener noreferrer">https://docs.erpnext.com/docs/v13/user/manual/en/e_commerce/set_up_e_commerce</a></li></ol><p><br></p></div>"""
+	note.insert(ignore_mandatory=True)
diff --git a/erpnext/patches/v14_0/migrate_cost_center_allocations.py b/erpnext/patches/v14_0/migrate_cost_center_allocations.py
index 3d217d8..c4f097f 100644
--- a/erpnext/patches/v14_0/migrate_cost_center_allocations.py
+++ b/erpnext/patches/v14_0/migrate_cost_center_allocations.py
@@ -27,7 +27,7 @@
 		cca.submit()
 
 def get_existing_cost_center_allocations():
-	if not frappe.get_meta("Cost Center").has_field("enable_distributed_cost_center"):
+	if not frappe.db.exists("DocType", "Distributed Cost Center"):
 		return
 
 	par = frappe.qb.DocType("Cost Center")
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index f33443d..f727ff4 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -746,11 +746,12 @@
 		previous_total_paid_taxes = self.get_tax_paid_in_period(payroll_period.start_date, self.start_date, tax_component)
 
 		# get taxable_earnings for current period (all days)
-		current_taxable_earnings = self.get_taxable_earnings(tax_slab.allow_tax_exemption)
+		current_taxable_earnings = self.get_taxable_earnings(tax_slab.allow_tax_exemption, payroll_period=payroll_period)
 		future_structured_taxable_earnings = current_taxable_earnings.taxable_earnings * (math.ceil(remaining_sub_periods) - 1)
 
 		# get taxable_earnings, addition_earnings for current actual payment days
-		current_taxable_earnings_for_payment_days = self.get_taxable_earnings(tax_slab.allow_tax_exemption, based_on_payment_days=1)
+		current_taxable_earnings_for_payment_days = self.get_taxable_earnings(tax_slab.allow_tax_exemption,
+			based_on_payment_days=1, payroll_period=payroll_period)
 		current_structured_taxable_earnings = current_taxable_earnings_for_payment_days.taxable_earnings
 		current_additional_earnings = current_taxable_earnings_for_payment_days.additional_income
 		current_additional_earnings_with_full_tax = current_taxable_earnings_for_payment_days.additional_income_with_full_tax
@@ -876,7 +877,7 @@
 
 		return total_tax_paid
 
-	def get_taxable_earnings(self, allow_tax_exemption=False, based_on_payment_days=0):
+	def get_taxable_earnings(self, allow_tax_exemption=False, based_on_payment_days=0, payroll_period=None):
 		joining_date, relieving_date = self.get_joining_and_relieving_dates()
 
 		taxable_earnings = 0
@@ -903,7 +904,7 @@
 					# Get additional amount based on future recurring additional salary
 					if additional_amount and earning.is_recurring_additional_salary:
 						additional_income += self.get_future_recurring_additional_amount(earning.additional_salary,
-							earning.additional_amount) # Used earning.additional_amount to consider the amount for the full month
+							earning.additional_amount, payroll_period) # Used earning.additional_amount to consider the amount for the full month
 
 					if earning.deduct_full_tax_on_selected_payroll_date:
 						additional_income_with_full_tax += additional_amount
@@ -920,7 +921,7 @@
 
 					if additional_amount and ded.is_recurring_additional_salary:
 						additional_income -= self.get_future_recurring_additional_amount(ded.additional_salary,
-							ded.additional_amount) # Used ded.additional_amount to consider the amount for the full month
+							ded.additional_amount, payroll_period) # Used ded.additional_amount to consider the amount for the full month
 
 		return frappe._dict({
 			"taxable_earnings": taxable_earnings,
@@ -929,12 +930,18 @@
 			"flexi_benefits": flexi_benefits
 		})
 
-	def get_future_recurring_additional_amount(self, additional_salary, monthly_additional_amount):
+	def get_future_recurring_additional_amount(self, additional_salary, monthly_additional_amount, payroll_period):
 		future_recurring_additional_amount = 0
 		to_date = frappe.db.get_value("Additional Salary", additional_salary, 'to_date')
 
 		# future month count excluding current
 		from_date, to_date = getdate(self.start_date), getdate(to_date)
+
+		# If recurring period end date is beyond the payroll period,
+		# last day of payroll period should be considered for recurring period calculation
+		if getdate(to_date) > getdate(payroll_period.end_date):
+			to_date = getdate(payroll_period.end_date)
+
 		future_recurring_period = ((to_date.year - from_date.year) * 12) + (to_date.month - from_date.month)
 
 		if future_recurring_period > 0:
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index bcf981b..597fd5a 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -147,7 +147,7 @@
 		# Payroll based on attendance
 		frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
 
-		emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company")
+		emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company", holiday_list="Salary Slip Test Holiday List")
 		frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"})
 
 		# mark attendance
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 1e056a6..8092ba2 100644
--- a/erpnext/portal/doctype/homepage/homepage.py
+++ b/erpnext/portal/doctype/homepage/homepage.py
@@ -14,12 +14,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 0e106c6..0000000
--- a/erpnext/portal/doctype/products_settings/products_settings.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-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 66026fc..0000000
--- a/erpnext/portal/doctype/products_settings/test_products_settings.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-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 b478489..0000000
--- a/erpnext/portal/product_configurator/test_product_configurator.py
+++ /dev/null
@@ -1,143 +0,0 @@
-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 974b51e..24bcab4 100644
--- a/erpnext/portal/utils.py
+++ b/erpnext/portal/utils.py
@@ -1,10 +1,10 @@
 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.doctype.e_commerce_settings.e_commerce_settings import (
 	get_shopping_cart_settings,
 )
+from erpnext.e_commerce.shopping_cart.cart import get_debtors_account
 
 
 def set_default_role(doc, method):
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index f8e8177..569910d 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -7,7 +7,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",
@@ -65,5 +66,11 @@
 	"js/hierarchy-chart.min.js": [
 		"public/js/hierarchy_chart/hierarchy_chart_desktop.js",
 		"public/js/hierarchy_chart/hierarchy_chart_mobile.js"
+	],
+	"js/e-commerce.min.js": [
+		"e_commerce/product_ui/views.js",
+		"e_commerce/product_ui/grid.js",
+		"e_commerce/product_ui/list.js",
+		"e_commerce/product_ui/search.js"
 	]
 }
diff --git a/erpnext/templates/includes/cart.js b/erpnext/public/js/cart.js
similarity index 85%
rename from erpnext/templates/includes/cart.js
rename to erpnext/public/js/cart.js
index c390cd1..69357ee 100644
--- a/erpnext/templates/includes/cart.js
+++ b/erpnext/public/js/cart.js
@@ -4,8 +4,8 @@
 // js inside blog page
 
 // shopping cart
-frappe.provide("erpnext.shopping_cart");
-var shopping_cart = erpnext.shopping_cart;
+frappe.provide("erpnext.e_commerce.shopping_cart");
+var shopping_cart = erpnext.e_commerce.shopping_cart;
 
 $.extend(shopping_cart, {
 	show_error: function(title, text) {
@@ -18,8 +18,8 @@
 		shopping_cart.bind_place_order();
 		shopping_cart.bind_request_quotation();
 		shopping_cart.bind_change_qty();
+		shopping_cart.bind_remove_cart_item();
 		shopping_cart.bind_change_notes();
-		shopping_cart.bind_dropdown_cart_buttons();
 		shopping_cart.bind_coupon_code();
 	},
 
@@ -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,
@@ -57,7 +57,7 @@
 					callback: function(r) {
 						d.hide();
 						if (!r.exc) {
-							$(".cart-tax-items").html(r.message.taxes);
+							$(".cart-tax-items").html(r.message.total);
 							shopping_cart.parent.find(
 								`.address-container[data-address-type="${address_type}"]`
 							).html(r.message.address);
@@ -129,8 +129,14 @@
 				}
 			}
 			input.val(newVal);
+
+			let notes = input.closest("td").siblings().find(".notes").text().trim();
 			var item_code = input.attr("data-item-code");
-			shopping_cart.shopping_cart_update({item_code, qty: newVal});
+			shopping_cart.shopping_cart_update({
+				item_code,
+				qty: newVal,
+				additional_notes: notes
+			});
 		});
 	},
 
@@ -148,6 +154,18 @@
 		});
 	},
 
+	bind_remove_cart_item: function() {
+		$(".cart-items").on("click", ".remove-cart-item", (e) => {
+			const $remove_cart_item_btn = $(e.currentTarget);
+			var item_code = $remove_cart_item_btn.data("item-code");
+
+			shopping_cart.shopping_cart_update({
+				item_code: item_code,
+				qty: 0
+			});
+		});
+	},
+
 	render_tax_row: function($cart_taxes, doc, shipping_rules) {
 		var shipping_selector;
 		if(shipping_rules) {
@@ -185,7 +203,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) {
@@ -196,12 +214,15 @@
 	},
 
 	place_order: function(btn) {
+		shopping_cart.freeze();
+
 		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) {
+					shopping_cart.unfreeze();
 					var msg = "";
 					if(r._server_messages) {
 						msg = JSON.parse(r._server_messages || []).join("<br>");
@@ -212,7 +233,6 @@
 						.html(msg || frappe._("Something went wrong!"))
 						.toggle(true);
 				} else {
-					$('.cart-container table').hide();
 					$(btn).hide();
 					window.location.href = '/orders/' + encodeURIComponent(r.message);
 				}
@@ -221,12 +241,15 @@
 	},
 
 	request_quotation: function(btn) {
+		shopping_cart.freeze();
+
 		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) {
+					shopping_cart.unfreeze();
 					var msg = "";
 					if(r._server_messages) {
 						msg = JSON.parse(r._server_messages || []).join("<br>");
@@ -237,7 +260,6 @@
 						.html(msg || frappe._("Something went wrong!"))
 						.toggle(true);
 				} else {
-					$('.cart-container table').hide();
 					$(btn).hide();
 					window.location.href = '/quotations/' + encodeURIComponent(r.message);
 				}
@@ -254,7 +276,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(),
@@ -270,7 +292,9 @@
 });
 
 frappe.ready(function() {
-	$(".cart-icon").hide();
+	if (window.location.pathname === "/cart") {
+		$(".cart-icon").hide();
+	}
 	shopping_cart.parent = $(".cart-container");
 	shopping_cart.bind_events();
 });
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/customer_reviews.js b/erpnext/public/js/customer_reviews.js
new file mode 100644
index 0000000..e13ded6
--- /dev/null
+++ b/erpnext/public/js/customer_reviews.js
@@ -0,0 +1,138 @@
+$(() => {
+	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 class="d-flex">
+							<p class="mr-4 user-review-title">
+								<span>${__(review.review_title)}</span>
+							</p>
+							<div class="rating">
+								${me.get_review_stars(review.rating)}
+							</div>
+						</div>
+
+						<div class="product-description mb-4">
+							<p>
+								${__(review.comment)}
+							</p>
+						</div>
+						<div class="review-signature mb-2">
+							<span class="reviewer">${__(review.customer)}</span>
+							<span class="indicator grey" style="--text-on-gray: var(--gray-300);"></span>
+							<span class="reviewer">${__(review.published_on)}</span>
+						</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-sm ${fill_class}">
+						<use href="#icon-star"></use>
+					</svg>
+				`;
+			}
+			return stars;
+		}
+	}
+
+	new CustomerReviews();
+});
\ No newline at end of file
diff --git a/erpnext/public/js/erpnext-web.bundle.js b/erpnext/public/js/erpnext-web.bundle.js
index 7db6967..576abd2 100644
--- a/erpnext/public/js/erpnext-web.bundle.js
+++ b/erpnext/public/js/erpnext-web.bundle.js
@@ -1,2 +1,9 @@
 import "./website_utils";
+import "./wishlist";
 import "./shopping_cart";
+import "./cart";
+import "./customer_reviews";
+import "../../e_commerce/product_ui/list";
+import "../../e_commerce/product_ui/views";
+import "../../e_commerce/product_ui/grid";
+import "../../e_commerce/product_ui/search";
\ No newline at end of file
diff --git a/erpnext/public/js/shopping_cart.js b/erpnext/public/js/shopping_cart.js
index 6a923ae..d14740c 100644
--- a/erpnext/public/js/shopping_cart.js
+++ b/erpnext/public/js/shopping_cart.js
@@ -2,8 +2,8 @@
 // License: GNU General Public License v3. See license.txt
 
 // shopping cart
-frappe.provide("erpnext.shopping_cart");
-var shopping_cart = erpnext.shopping_cart;
+frappe.provide("erpnext.e_commerce.shopping_cart");
+var shopping_cart = erpnext.e_commerce.shopping_cart;
 
 var getParams = function (url) {
 	var params = [];
@@ -51,10 +51,10 @@
 	if (referral_sales_partner) {
 		$(".txtreferral_sales_partner").val(referral_sales_partner);
 	}
+
 	// update login
 	shopping_cart.show_shoppingcart_dropdown();
 	shopping_cart.set_cart_count();
-	shopping_cart.bind_dropdown_cart_buttons();
 	shopping_cart.show_cart_navbar();
 });
 
@@ -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);
@@ -75,15 +75,18 @@
 	},
 
 	update_cart: function(opts) {
-		if(frappe.session.user==="Guest") {
-			if(localStorage) {
+		if (frappe.session.user==="Guest") {
+			if (localStorage) {
 				localStorage.setItem("last_visited", window.location.pathname);
 			}
-			window.location.href = "/login";
+			frappe.call('erpnext.e_commerce.api.get_guest_redirect_on_action').then((res) => {
+				window.location.href = res.message || "/login";
+			});
 		} else {
+			shopping_cart.freeze();
 			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,
@@ -92,10 +95,8 @@
 				},
 				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);
-					}
+					shopping_cart.unfreeze();
+					shopping_cart.set_cart_count(true);
 					if(opts.callback)
 						opts.callback(r);
 				}
@@ -103,7 +104,9 @@
 		}
 	},
 
-	set_cart_count: function() {
+	set_cart_count: function(animate=false) {
+		$(".intermediate-empty-cart").remove();
+
 		var cart_count = frappe.get_cookie("cart_count");
 		if(frappe.session.user==="Guest") {
 			cart_count = 0;
@@ -118,24 +121,37 @@
 
 		if(parseInt(cart_count) === 0 || cart_count === undefined) {
 			$cart.css("display", "none");
-			$(".cart-items").html('Cart is Empty');
 			$(".cart-tax-items").hide();
 			$(".btn-place-order").hide();
-			$(".cart-addresses").hide();
+			$(".cart-payment-addresses").hide();
+
+			let intermediate_empty_cart_msg = `
+				<div class="text-center w-100 intermediate-empty-cart mt-4 mb-4 text-muted">
+					${ __("Cart is Empty") }
+				</div>
+			`;
+			$(".cart-table").after(intermediate_empty_cart_msg);
 		}
 		else {
 			$cart.css("display", "inline");
+			$("#cart-count").text(cart_count);
 		}
 
 		if(cart_count) {
 			$badge.html(cart_count);
+
+			if (animate) {
+				$cart.addClass("cart-animate");
+				setTimeout(() => {
+					$cart.removeClass("cart-animate");
+				}, 500);
+			}
 		} else {
 			$badge.remove();
 		}
 	},
 
 	shopping_cart_update: function({item_code, qty, cart_dropdown, additional_notes}) {
-		frappe.freeze();
 		shopping_cart.update_cart({
 			item_code,
 			qty,
@@ -143,10 +159,12 @@
 			with_items: 1,
 			btn: this,
 			callback: function(r) {
-				frappe.unfreeze();
 				if(!r.exc) {
 					$(".cart-items").html(r.message.items);
-					$(".cart-tax-items").html(r.message.taxes);
+					$(".cart-tax-items").html(r.message.total);
+					$(".payment-summary").html(r.message.taxes_and_totals);
+					shopping_cart.set_cart_count();
+
 					if (cart_dropdown != true) {
 						$(".cart-icon").hide();
 					}
@@ -155,35 +173,71 @@
 		});
 	},
 
-
-	bind_dropdown_cart_buttons: function () {
-		$(".cart-icon").on('click', '.number-spinner button', function () {
-			var btn = $(this),
-				input = btn.closest('.number-spinner').find('input'),
-				oldValue = input.val().trim(),
-				newVal = 0;
-
-			if (btn.attr('data-dir') == 'up') {
-				newVal = parseInt(oldValue) + 1;
-			} else {
-				if (oldValue > 1) {
-					newVal = parseInt(oldValue) - 1;
-				}
-			}
-			input.val(newVal);
-			var item_code = input.attr("data-item-code");
-			shopping_cart.shopping_cart_update({item_code, qty: newVal, cart_dropdown: true});
-			return false;
-		});
-
-	},
-
 	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);
 			}
 		});
+	},
+
+	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);
+
+			if (frappe.session.user==="Guest") {
+				if (localStorage) {
+					localStorage.setItem("last_visited", window.location.pathname);
+				}
+				frappe.call('erpnext.e_commerce.api.get_guest_redirect_on_action').then((res) => {
+					window.location.href = res.message || "/login";
+				});
+				return;
+			}
+
+			$btn.addClass('hidden');
+			$btn.closest('.cart-action-container').addClass('d-flex');
+			$btn.parent().find('.go-to-cart').removeClass('hidden');
+			$btn.parent().find('.go-to-cart-grid').removeClass('hidden');
+			$btn.parent().find('.cart-indicator').removeClass('hidden');
+
+			const item_code = $btn.data('item-code');
+			erpnext.e_commerce.shopping_cart.update_cart({
+				item_code,
+				qty: 1
+			});
+
+		});
+	},
+
+	freeze() {
+		if (window.location.pathname !== "/cart") return;
+
+		if (!$('#freeze').length) {
+			let freeze = $('<div id="freeze" class="modal-backdrop fade"></div>')
+				.appendTo("body");
+
+			setTimeout(function() {
+				freeze.addClass("show");
+			}, 1);
+		} else {
+			$("#freeze").addClass("show");
+		}
+	},
+
+	unfreeze() {
+		if ($('#freeze').length) {
+			let freeze = $('#freeze').removeClass("show");
+			setTimeout(function() {
+				freeze.remove();
+			}, 1);
+		}
 	}
 });
diff --git a/erpnext/public/js/wishlist.js b/erpnext/public/js/wishlist.js
new file mode 100644
index 0000000..f6599e9
--- /dev/null
+++ b/erpnext/public/js/wishlist.js
@@ -0,0 +1,204 @@
+frappe.provide("erpnext.e_commerce.wishlist");
+var wishlist = erpnext.e_commerce.wishlist;
+
+frappe.provide("erpnext.e_commerce.shopping_cart");
+var shopping_cart = erpnext.e_commerce.shopping_cart;
+
+$.extend(wishlist, {
+	set_wishlist_count: function(animate=false) {
+		// 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);
+			if (animate) {
+				$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
+		let me = this;
+
+		$('.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");
+				if (frappe.get_cookie("wish_count") == 0) {
+					$(".page_content").empty();
+					me.render_empty_state();
+				}
+			};
+			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, .like-action-list', (e) => {
+			const $btn = $(e.currentTarget);
+			this.wishlist_action($btn);
+		});
+	},
+
+	wishlist_action(btn) {
+		const $wish_icon = btn.find('.wish-icon');
+		let me = this;
+
+		if (frappe.session.user==="Guest") {
+			if (localStorage) {
+				localStorage.setItem("last_visited", window.location.pathname);
+			}
+			this.redirect_guest();
+			return;
+		}
+
+		let success_action = function() {
+			erpnext.e_commerce.wishlist.set_wishlist_count(true);
+		};
+
+		if ($wish_icon.hasClass('wished')) {
+			// un-wish item
+			btn.removeClass("like-animate");
+			btn.addClass("like-action-wished");
+			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");
+			btn.addClass("like-action-wished");
+			this.toggle_button_class($wish_icon, 'not-wished', 'wished');
+
+			let args = {item_code: btn.data('item-code')};
+			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).	*/
+		if (frappe.session.user==="Guest") {
+			if (localStorage) {
+				localStorage.setItem("last_visited", window.location.pathname);
+			}
+			this.redirect_guest();
+		} else {
+			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();
+					}
+				}
+			});
+		}
+	},
+
+	redirect_guest() {
+		frappe.call('erpnext.e_commerce.api.get_guest_redirect_on_action').then((res) => {
+			window.location.href = res.message || "/login";
+		});
+	},
+
+	render_empty_state() {
+		$(".page_content").append(`
+			<div class="cart-empty frappe-card">
+				<div class="cart-empty-state">
+					<img src="/assets/erpnext/images/ui-states/cart-empty-state.png" alt="Empty Cart">
+				</div>
+				<div class="cart-empty-message mt-4">${ __('Wishlist is empty !') }</p>
+			</div>
+		`);
+	}
+
+});
+
+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..4b645b9 100644
--- a/erpnext/public/scss/shopping_cart.scss
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -1,16 +1,17 @@
 @import "frappe/public/scss/common/mixins";
 
-body.product-page {
-	background: var(--gray-50);
+:root {
+	--green-info: #38A160;
+	--product-bg-color: white;
+	--body-bg-color:  var(--gray-50);
 }
 
+body.product-page {
+	background: var(--body-bg-color);
+}
 
 .item-breadcrumbs {
 	.breadcrumb-container {
-		ol.breadcrumb {
-			background-color: var(--gray-50) !important;
-		}
-
 		a {
 			color: var(--gray-900);
 		}
@@ -71,9 +72,21 @@
 	}
 }
 
+.no-image-item {
+	height: 340px;
+	width: 340px;
+	background: var(--gray-100);
+	border-radius: var(--border-radius);
+	font-size: 2rem;
+	color: var(--gray-500);
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
+
 .item-card-group-section {
 	.card {
-		height: 360px;
+		height: 100%;
 		align-items: center;
 		justify-content: center;
 
@@ -83,6 +96,19 @@
 		}
 	}
 
+	.card:hover, .card:focus-within {
+		.btn-add-to-cart-list {
+			visibility: visible;
+		}
+		.like-action {
+			visibility: visible;
+		}
+		.btn-explore-variants {
+			visibility: visible;
+		}
+	}
+
+
 	.card-img-container {
 		height: 210px;
 		width: 100%;
@@ -96,14 +122,28 @@
 
 	.no-image {
 		@include flex(flex, center, center, null);
-		height: 200px;
-		margin: 0 auto;
-		margin-top: var(--margin-xl);
+		height: 220px;
 		background: var(--gray-100);
-		width: 80%;
+		width: 100%;
+		border-radius: var(--border-radius) var(--border-radius) 0 0;
+		font-size: 2rem;
+		color: var(--gray-500);
+	}
+
+	.no-image-list {
+		@include flex(flex, center, center, null);
+		height: 150px;
+		background: var(--gray-100);
 		border-radius: var(--border-radius);
 		font-size: 2rem;
 		color: var(--gray-500);
+		margin-top: 15px;
+		margin-bottom: 15px;
+	}
+
+	.card-body-flex {
+		display: flex;
+		flex-direction: column;
 	}
 
 	.product-title {
@@ -136,15 +176,75 @@
 		font-weight: 600;
 		color: var(--text-color);
 		margin: var(--margin-sm) 0;
+		margin-bottom: auto !important;
+
+		.striked-price {
+			font-weight: 500;
+			font-size: 15px;
+			color: var(--gray-500);
+		}
+	}
+
+	.product-info-green {
+		color: var(--green-info);
+		font-weight: 600;
 	}
 
 	.item-card {
 		padding: var(--padding-sm);
+		min-width: 300px;
+	}
+
+	.wishlist-card {
+		padding: var(--padding-sm);
+		min-width: 260px;
+		.card-body-flex {
+			display: flex;
+			flex-direction: column;
+		}
+	}
+}
+
+#products-list-area, #products-grid-area {
+	padding: 0 5px;
+}
+
+.list-row {
+	background-color: white;
+	padding-bottom: 1rem;
+	padding-top: 1.5rem !important;
+	border-radius: 8px;
+	border-bottom: 1px solid var(--gray-50);
+
+	&:hover, &:focus-within {
+		box-shadow: 0px 16px 60px rgba(0, 0, 0, 0.08), 0px 8px 30px -20px rgba(0, 0, 0, 0.04);
+		transition: box-shadow 400ms;
+
+		.btn-add-to-cart-list {
+			visibility: visible;
+		}
+		.like-action-list {
+			visibility: visible;
+		}
+		.btn-explore-variants {
+			visibility: visible;
+		}
+	}
+
+	.product-code {
+		padding-top: 0 !important;
+	}
+
+	.btn-explore-variants {
+		min-width: 135px;
+		max-height: 30px;
+		float: right;
+		padding: 0.25rem 1rem;
 	}
 }
 
 [data-doctype="Item Group"],
-#page-all-products {
+#page-index {
 	.page-header {
 		font-size: 20px;
 		font-weight: 700;
@@ -184,28 +284,76 @@
 	}
 }
 
+.product-filter {
+	width: 14px !important;
+	height: 14px !important;
+}
+
+.discount-filter {
+	&:before {
+		width: 14px !important;
+		height: 14px !important;
+	}
+}
+
+.list-image {
+	border: none !important;
+	overflow: hidden;
+	max-height: 200px;
+	background-color: white;
+}
+
 .product-container {
 	@include card($padding: var(--padding-md));
-	min-height: 70vh;
+	background-color: var(--product-bg-color) !important;
+	min-height: fit-content;
 
 	.product-details {
-		max-width: 40%;
-		margin-left: -30px;
+		max-width: 50%;
 
 		.btn-add-to-cart {
-			font-size: var(--text-base);
+			font-size: 14px;
+		}
+	}
+
+	&.item-main {
+		.product-image {
+			width: 100%;
+		}
+	}
+
+	.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: 14px;
+			}
+		}
+	}
+
+	.btn-add-to-wishlist {
+		svg use {
+			stroke: #F47A7A;
+		}
+	}
+
+	.btn-view-in-wishlist {
+		svg use {
+			fill: #F47A7A;
+			stroke: none;
 		}
 	}
 
 	.product-title {
-		font-size: 24px;
+		font-size: 16px;
 		font-weight: 600;
 		color: var(--text-color);
-	}
-
-	.product-code {
-		color: var(--text-muted);
-		font-size: 13px;
+		padding: 0 !important;
 	}
 
 	.product-description {
@@ -242,7 +390,7 @@
 			max-height: 430px;
 		}
 
-		overflow: scroll;
+		overflow: auto;
 	}
 
 	.item-slideshow-image {
@@ -261,29 +409,116 @@
 
 	.item-cart {
 		.product-price {
-			font-size: 20px;
+			font-size: 22px;
 			color: var(--text-color);
 			font-weight: 600;
 
 			.formatted-price {
 				color: var(--text-muted);
-				font-size: var(--text-base);
+				font-size: 14px;
 			}
 		}
 
 		.no-stock {
 			font-size: var(--text-base);
 		}
+
+		.offers-heading {
+			font-size: 16px !important;
+			color: var(--text-color);
+			.tag-icon {
+				--icon-stroke: var(--gray-500);
+			}
+		}
+
+		.w-30-40 {
+			width: 30%;
+
+			@media (max-width: 992px) {
+				width: 40%;
+			}
+		}
+	}
+
+	.tab-content {
+		font-size: 14px;
+	}
+}
+
+// Item Recommendations
+.recommended-item-section {
+	padding-right: 0;
+
+	.recommendation-header {
+		font-size: 16px;
+		font-weight: 500
+	}
+
+	.recommendation-container {
+		padding: .5rem;
+		min-height: 0px;
+
+		.r-item-image {
+			min-height: 100px;
+			width: 40%;
+
+			.r-product-image {
+				padding: 2px 15px;
+			}
+
+			.no-image-r-item {
+				display: flex; justify-content: center;
+				background-color: var(--gray-200);
+				align-items: center;
+				color: var(--gray-400);
+				margin-top: .15rem;
+				border-radius: 6px;
+				height: 100%;
+				font-size: 24px;
+			}
+		}
+
+		.r-item-info {
+			font-size: 14px;
+			padding-right: 0;
+			padding-left: 10px;
+			width: 60%;
+
+			a {
+				color: var(--gray-800);
+				font-weight: 400;
+			}
+
+			.item-price {
+				font-size: 15px;
+				font-weight: 600;
+				color: var(--text-color);
+			}
+
+			.striked-item-price {
+				font-weight: 500;
+				color: var(--gray-500);
+			}
+		}
+	}
+}
+
+.product-code {
+	padding: .5rem 0;
+	color: var(--text-muted);
+	font-size: 14px;
+	.product-item-group {
+		padding-right: .25rem;
+		border-right: solid 1px var(--text-muted);
+	}
+
+	.product-item-code {
+		padding-left: .5rem;
 	}
 }
 
 .item-configurator-dialog {
-	.modal-header {
-		padding: var(--padding-md) var(--padding-xl);
-	}
-
 	.modal-body {
-		padding: 0 var(--padding-xl);
 		padding-bottom: var(--padding-xl);
 
 		.status-area {
@@ -323,20 +558,73 @@
 	}
 }
 
-.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: .5rem;
+	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);
+	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);
+	}
+}
+
+.total-discount {
+	font-size: 14px;
+	color: var(--primary-color) !important;
+}
 
 #page-cart {
 	.shopping-cart-header {
@@ -350,6 +638,7 @@
 			display: flex;
 			flex-direction: column;
 			justify-content: space-between;
+			height: fit-content;
 		}
 
 		.cart-items-header {
@@ -357,6 +646,10 @@
 		}
 
 		.cart-table {
+			tr {
+				margin-bottom: 1rem;
+			}
+
 			th, tr, td {
 				border-color: var(--border-color);
 				border-width: 1px;
@@ -374,71 +667,200 @@
 				color: var(--text-color);
 			}
 
+			.cart-item-image {
+				width: 20%;
+				min-width: 100px;
+				img {
+					max-height: 112px;
+				}
+			}
+
 			.cart-items {
 				.item-title {
-					font-size: var(--text-base);
+					width: 80%;
+					font-size: 14px;
 					font-weight: 500;
 					color: var(--text-color);
 				}
 
 				.item-subtitle {
 					color: var(--text-muted);
-					font-size: var(--text-md);
+					font-size: 13px;
 				}
 
 				.item-subtotal {
-					font-size: var(--text-base);
+					font-size: 14px;
 					font-weight: 500;
 				}
 
+				.sm-item-subtotal {
+					font-size: 14px;
+					font-weight: 500;
+					display: none;
+
+					@media (max-width: 992px) {
+						display: unset !important;
+					}
+				}
+
 				.item-rate {
-					font-size: var(--text-md);
+					font-size: 13px;
 					color: var(--text-muted);
 				}
 
-				textarea {
-					width: 40%;
+				.free-tag {
+					padding: 4px 8px;
+					border-radius: 4px;
+					background-color: var(--dark-green-50);
 				}
+
+				textarea {
+					width: 80%;
+					height: 60px;
+					font-size: 14px;
+				}
+
 			}
 
 			.cart-tax-items {
 				.item-grand-total {
 					font-size: 16px;
-					font-weight: 600;
+					font-weight: 700;
 					color: var(--text-color);
 				}
 			}
+
+			.column-sm-view {
+				@media (max-width: 992px) {
+					display: none !important;
+				}
+			}
+
+			.item-column {
+				width: 50%;
+				@media (max-width: 992px) {
+					width: 70%;
+				}
+			}
+
+			.remove-cart-item {
+				border-radius: 6px;
+				border: 1px solid var(--gray-100);
+				width: 28px;
+				height: 28px;
+				font-weight: 300;
+				color: var(--gray-700);
+				background-color: var(--gray-100);
+				float: right;
+				cursor: pointer;
+				margin-top: .25rem;
+				justify-content: center;
+			}
+
+			.remove-cart-item-logo {
+				margin-top: 2px;
+				margin-left: 2.2px;
+				fill: var(--gray-700) !important;
+			}
 		}
 
-		.cart-addresses {
+		.cart-payment-addresses {
 			hr {
 				border-color: var(--border-color);
 			}
 		}
 
+		.payment-summary {
+			h6 {
+				padding-bottom: 1rem;
+				border-bottom: solid 1px var(--gray-200);
+			}
+
+			table {
+				font-size: 14px;
+				td {
+					padding: 0;
+					padding-top: 0.35rem !important;
+					border: none !important;
+				}
+
+				&.grand-total {
+					border-top: solid 1px var(--gray-200);
+				}
+			}
+
+			.bill-label {
+				color: var(--gray-600);
+			}
+
+			.bill-content {
+				font-weight: 500;
+				&.net-total {
+					font-size: 16px;
+					font-weight: 600;
+				}
+			}
+
+			.btn-coupon-code {
+				font-size: 14px;
+				border: dashed 1px var(--gray-400);
+				box-shadow: none;
+			}
+		}
+
 		.number-spinner {
 			width: 75%;
+			min-width: 105px;
 			.cart-btn {
 				border: none;
 				background: var(--gray-100);
 				box-shadow: none;
+				width: 24px;
 				height: 28px;
 				align-items: center;
+				justify-content: center;
 				display: flex;
+				font-size: 20px;
+				font-weight: 300;
+				color: var(--gray-700);
 			}
 
 			.cart-qty {
 				height: 28px;
-				font-size: var(--text-md);
+				font-size: 13px;
+				&:disabled {
+					background: var(--gray-100);
+					opacity: 0.65;
+				}
 			}
 		}
 
 		.place-order-container {
 			.btn-place-order {
-				width: 62%;
+				float: right;
 			}
 		}
 	}
+
+	.t-and-c-container {
+		padding: 1.5rem;
+	}
+
+	.t-and-c-terms {
+		font-size: 14px;
+	}
+}
+
+.no-image-cart-item {
+	max-height: 112px;
+	display: flex; justify-content: center;
+	background-color: var(--gray-200);
+	align-items: center;
+	color: var(--gray-400);
+	margin-top: .15rem;
+	border-radius: 6px;
+	height: 100%;
+	font-size: 24px;
 }
 
 .cart-empty.frappe-card {
@@ -454,7 +876,7 @@
 
 .address-card {
 	.card-title {
-		font-size: var(--text-base);
+		font-size: 14px;
 		font-weight: 500;
 	}
 
@@ -463,27 +885,37 @@
 	}
 
 	.card-text {
-		font-size: var(--text-md);
+		font-size: 13px;
 		color: var(--gray-700);
 	}
 
 	.card-link {
-		font-size: var(--text-md);
+		font-size: 13px;
 
 		svg use {
-			stroke: var(--blue-500);
+			stroke: var(--primary-color);
 		}
 	}
 
 	.btn-change-address {
-		color: var(--blue-500);
+		border: 1px solid var(--primary-color);
+		color: var(--primary-color);
+		box-shadow: none;
 	}
 }
 
+.address-header {
+	margin-top: .15rem;padding: 0;
+}
+
+.btn-new-address {
+	float: right;
+	font-size: 15px !important;
+	color: var(--primary-color) !important;
+}
+
 .btn-new-address:hover, .btn-change-address:hover {
-	box-shadow: none;
-	color: var(--blue-500) !important;
-	border: 1px solid var(--blue-500);
+	color: var(--primary-color) !important;
 }
 
 .modal .address-card {
@@ -493,3 +925,451 @@
 		border: 1px solid var(--dark-border-color);
 	}
 }
+
+.cart-indicator {
+	position: absolute;
+	text-align: center;
+	width: 22px;
+	height: 22px;
+	left: calc(100% - 40px);
+	top: 22px;
+
+	border-radius: 66px;
+	box-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1);
+	background: white;
+	color: var(--primary-color);
+	font-size: 14px;
+
+	&.list-indicator {
+		position: unset;
+		margin-left: auto;
+	}
+}
+
+
+.like-action {
+	visibility: hidden;
+	text-align: center;
+	position: absolute;
+	cursor: pointer;
+	width: 28px;
+	height: 28px;
+	left: 20px;
+	top: 20px;
+
+	/* White */
+	background: white;
+	box-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1);
+	border-radius: 66px;
+
+	&.like-action-wished {
+		visibility: visible !important;
+	}
+
+	@media (max-width: 992px) {
+		visibility: visible !important;
+	}
+}
+
+.like-action-list {
+	visibility: hidden;
+	text-align: center;
+	position: absolute;
+	cursor: pointer;
+	width: 28px;
+	height: 28px;
+	left: 20px;
+	top: 0;
+
+	/* White */
+	background: white;
+	box-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1);
+	border-radius: 66px;
+
+	&.like-action-wished {
+		visibility: visible !important;
+	}
+
+	@media (max-width: 992px) {
+		visibility: visible !important;
+	}
+}
+
+.like-action-item-fp {
+	visibility: visible !important;
+	position: unset;
+	float: right;
+}
+
+.like-animate {
+	animation: expand cubic-bezier(0.04, 0.4, 0.5, 0.95) 1.6s forwards 1;
+}
+
+@keyframes expand {
+	30% {
+	  transform: scale(1.3);
+	}
+	50% {
+	  transform: scale(0.8);
+	}
+	70% {
+		transform: scale(1.1);
+	}
+	100% {
+	  transform: scale(1);
+	}
+  }
+
+.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;
+	}
+}
+
+#pay-for-order {
+	padding: .5rem 1rem; // Pay button in SO
+}
+
+.btn-explore-variants {
+	visibility: hidden;
+	box-shadow: none;
+	margin: var(--margin-sm) 0;
+	width: 90px;
+	max-height: 50px; // to avoid resizing on window resize
+	flex: none;
+	transition: 0.3s ease;
+
+	color: white;
+	background-color: var(--orange-500);
+	border: 1px solid var(--orange-500);
+	font-size: 13px;
+
+	&:hover {
+		color: white;
+	}
+}
+
+.btn-add-to-cart-list{
+	visibility: hidden;
+	box-shadow: none;
+	margin: var(--margin-sm) 0;
+	// margin-top: auto !important;
+	max-height: 50px; // to avoid resizing on window resize
+	flex: none;
+	transition: 0.3s ease;
+
+	font-size: 13px;
+
+	&:hover {
+		color: white;
+	}
+
+	@media (max-width: 992px) {
+		visibility: visible !important;
+	}
+}
+
+.go-to-cart-grid {
+	max-height: 30px;
+	margin-top: 1rem !important;
+}
+
+.go-to-cart {
+	max-height: 30px;
+	float: right;
+}
+
+.remove-wish {
+	background-color: white;
+	position: absolute;
+	cursor: pointer;
+	top:10px;
+	right: 20px;
+	width: 32px;
+	height: 32px;
+
+	border-radius: 50%;
+	border: 1px solid var(--gray-100);
+	box-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1);
+}
+
+.wish-removed {
+	display: none;
+}
+
+.item-website-specification {
+	font-size: .875rem;
+	.product-title {
+		font-size: 18px;
+	}
+
+	.table {
+		width: 70%;
+	}
+
+	td {
+		border: none !important;
+	}
+
+	.spec-label {
+		color: var(--gray-600);
+	}
+
+	.spec-content {
+		color: var(--gray-800);
+	}
+}
+
+.reviews-full-page {
+	padding: 1rem 2rem;
+}
+
+.ratings-reviews-section {
+	border-top: 1px solid #E2E6E9;
+	padding: .5rem 1rem;
+}
+
+.reviews-header {
+	font-size: 20px;
+	font-weight: 600;
+	color: var(--gray-800);
+	display: flex;
+	align-items: center;
+	padding: 0;
+}
+
+.btn-write-review {
+	float: right;
+	padding: .5rem 1rem;
+	font-size: 14px;
+	font-weight: 400;
+	border: none !important;
+	box-shadow: none;
+
+	color: var(--gray-900);
+	background-color: var(--gray-100);
+
+	&:hover {
+		box-shadow: var(--btn-shadow);
+	}
+}
+
+.btn-view-more {
+	font-size: 14px;
+}
+
+.rating-summary-section {
+	display: flex;
+}
+
+.rating-summary-title {
+	margin-top: 0.15rem;
+	font-size: 18px;
+}
+
+.rating-summary-numbers {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+
+	border-right: solid 1px var(--gray-100);
+}
+
+.user-review-title {
+	margin-top: 0.15rem;
+	font-size: 15px;
+	font-weight: 600;
+}
+
+.rating {
+	--star-fill: var(--gray-300);
+	.star-hover {
+		--star-fill: var(--yellow-100);
+	}
+	.star-click {
+		--star-fill: var(--yellow-300);
+	}
+}
+
+.ratings-pill {
+	background-color: var(--gray-100);
+	padding: .5rem 1rem;
+	border-radius: 66px;
+}
+
+.review {
+	max-width: 80%;
+	line-height: 1.6;
+	padding-bottom: 0.5rem;
+	border-bottom: 1px solid #E2E6E9;
+}
+
+.review-signature {
+	display: flex;
+	font-size: 13px;
+	color: var(--gray-500);
+	font-weight: 400;
+
+	.reviewer {
+		padding-right: 8px;
+		color: var(--gray-600);
+	}
+}
+
+.rating-progress-bar-section {
+	padding-bottom: 2rem;
+
+	.rating-bar-title {
+		margin-left: -15px;
+	}
+
+	.rating-progress-bar {
+		margin-bottom: 4px;
+		height: 7px;
+		margin-top: 6px;
+
+		.progress-bar-cosmetic {
+			background-color: var(--gray-600);
+			border-radius: var(--border-radius);
+		}
+	}
+}
+
+.offer-container {
+	font-size: 14px;
+}
+
+#search-results-container {
+	border: 1px solid var(--gray-200);
+	padding: .25rem 1rem;
+
+	.category-chip {
+		background-color: var(--gray-100);
+		border: none !important;
+		box-shadow: none;
+	}
+
+	.recent-search {
+		padding: .5rem .5rem;
+		border-radius: var(--border-radius);
+
+		&:hover {
+			background-color: var(--gray-100);
+		}
+	}
+}
+
+#search-box {
+	background-color: white;
+	height: 100%;
+	padding-left: 2.5rem;
+	border: 1px solid var(--gray-200);
+}
+
+.search-icon {
+	position: absolute;
+	left: 0;
+	top: 0;
+	width: 2.5rem;
+	height: 100%;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	padding-bottom: 1px;
+}
+
+#toggle-view {
+	float: right;
+
+	.btn-primary {
+		background-color: var(--gray-600);
+		box-shadow: 0 0 0 0.2rem var(--gray-400);
+	}
+}
+
+.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;
+}
+
+[data-path="cart"] {
+	.modal-backdrop {
+		background-color: var(--gray-50); // lighter backdrop only on cart freeze
+	}
+}
+
+.item-thumb {
+	height: 50px;
+	max-width: 80px;
+	min-width: 80px;
+	object-fit: cover;
+}
+
+.brand-line {
+	color: gray;
+}
+
+.btn-next, .btn-prev {
+	font-size: 14px;
+}
+
+.alert-error {
+	color: #e27a84;
+	background-color: #fff6f7;
+	border-color: #f5c6cb;
+}
+
+.font-md {
+	font-size: 14px !important;
+}
+
+.in-green {
+	color: var(--green-info) !important;
+	font-weight: 500;
+}
+
+.has-stock {
+	font-weight: 400 !important;
+}
+
+.out-of-stock {
+	font-weight: 400;
+	font-size: 14px;
+	line-height: 20px;
+	color: #F47A7A;
+}
+
+.mt-minus-2 {
+	margin-top: -2rem;
+}
+
+.mt-minus-1 {
+	margin-top: -1rem;
+}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index d74d5a6..7742f26 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -258,30 +258,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 daab6fb..eebde76 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -287,7 +287,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/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json
index 8b53902..31a9589 100644
--- a/erpnext/selling/doctype/quotation_item/quotation_item.json
+++ b/erpnext/selling/doctype/quotation_item/quotation_item.json
@@ -649,7 +649,7 @@
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2021-02-23 01:13:54.670763",
+ "modified": "2021-07-15 12:40:51.074820",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Quotation Item",
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/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py
index db5b20e..993c61d 100644
--- a/erpnext/selling/page/point_of_sale/point_of_sale.py
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.py
@@ -24,7 +24,7 @@
 			["name as item_code", "item_name", "description", "stock_uom", "image as item_image", "is_stock_item"],
 			as_dict=1)
 
-		item_stock_qty = get_stock_availability(item_code, warehouse)
+		item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
 		price_list_rate, currency = frappe.db.get_value('Item Price', {
 			'price_list': price_list,
 			'item_code': item_code
@@ -99,7 +99,6 @@
 		), {'warehouse': warehouse}, as_dict=1)
 
 	if items_data:
-		items_data = filter_service_items(items_data)
 		items = [d.item_code for d in items_data]
 		item_prices_data = frappe.get_all("Item Price",
 			fields = ["item_code", "price_list_rate", "currency"],
@@ -112,7 +111,7 @@
 		for item in items_data:
 			item_code = item.item_code
 			item_price = item_prices.get(item_code) or {}
-			item_stock_qty = get_stock_availability(item_code, warehouse)
+			item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
 
 			row = {}
 			row.update(item)
@@ -144,14 +143,6 @@
 
 	return {}
 
-def filter_service_items(items):
-	for item in items:
-		if not item['is_stock_item']:
-			if not frappe.db.exists('Product Bundle', item['item_code']):
-				items.remove(item)
-
-	return items
-
 def get_conditions(search_term):
 	condition = "("
 	condition += """item.name like {search_term}
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index ce74f6d..ea8459f 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -248,7 +248,7 @@
 
 				numpad_event: (value, action) => this.update_item_field(value, action),
 
-				checkout: () => this.payment.checkout(),
+				checkout: () => this.save_and_checkout(),
 
 				edit_cart: () => this.payment.edit_cart(),
 
@@ -630,18 +630,24 @@
 	}
 
 	async check_stock_availability(item_row, qty_needed, warehouse) {
-		const available_qty = (await this.get_available_stock(item_row.item_code, warehouse)).message;
+		const resp = (await this.get_available_stock(item_row.item_code, warehouse)).message;
+		const available_qty = resp[0];
+		const is_stock_item = resp[1];
 
 		frappe.dom.unfreeze();
 		const bold_item_code = item_row.item_code.bold();
 		const bold_warehouse = warehouse.bold();
 		const bold_available_qty = available_qty.toString().bold()
 		if (!(available_qty > 0)) {
-			frappe.model.clear_doc(item_row.doctype, item_row.name);
-			frappe.throw({
-				title: __("Not Available"),
-				message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse])
-			})
+			if (is_stock_item) {
+				frappe.model.clear_doc(item_row.doctype, item_row.name);
+				frappe.throw({
+					title: __("Not Available"),
+					message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse])
+				});
+			} else {
+				return;
+			}
 		} else if (available_qty < qty_needed) {
 			frappe.throw({
 				message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]),
@@ -675,8 +681,8 @@
 			},
 			callback(res) {
 				if (!me.item_stock_map[item_code])
-					me.item_stock_map[item_code] = {}
-				me.item_stock_map[item_code][warehouse] = res.message;
+					me.item_stock_map[item_code] = {};
+				me.item_stock_map[item_code][warehouse] = res.message[0];
 			}
 		});
 	}
@@ -707,4 +713,9 @@
 			})
 			.catch(e => console.log(e));
 	}
+
+	async save_and_checkout() {
+		this.frm.is_dirty() && await this.frm.save();
+		this.payment.checkout();
+	}
 };
diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
index 4920584..4a99f06 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -191,10 +191,10 @@
 			this.numpad_value = '';
 		});
 
-		this.$component.on('click', '.checkout-btn', function() {
+		this.$component.on('click', '.checkout-btn', async function() {
 			if ($(this).attr('style').indexOf('--blue-500') == -1) return;
 
-			me.events.checkout();
+			await me.events.checkout();
 			me.toggle_checkout_btn(false);
 
 			me.allow_discount_change && me.$add_discount_elem.removeClass("d-none");
@@ -985,6 +985,7 @@
 		$(frm.wrapper).off('refresh-fields');
 		$(frm.wrapper).on('refresh-fields', () => {
 			if (frm.doc.items.length) {
+				this.$cart_items_wrapper.html('');
 				frm.doc.items.forEach(item => {
 					this.update_item_html(item);
 				});
diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js
index a30bcd7..1177615 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_selector.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js
@@ -79,14 +79,20 @@
 		const me = this;
 		// eslint-disable-next-line no-unused-vars
 		const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom, price_list_rate } = item;
-		const indicator_color = actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange";
 		const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0;
-
+		let indicator_color;
 		let qty_to_display = actual_qty;
 
-		if (Math.round(qty_to_display) > 999) {
-			qty_to_display = Math.round(qty_to_display)/1000;
-			qty_to_display = qty_to_display.toFixed(1) + 'K';
+		if (item.is_stock_item) {
+			indicator_color = (actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange");
+
+			if (Math.round(qty_to_display) > 999) {
+				qty_to_display = Math.round(qty_to_display)/1000;
+				qty_to_display = qty_to_display.toFixed(1) + 'K';
+			}
+		} else {
+			indicator_color = '';
+			qty_to_display = '';
 		}
 
 		function get_item_image_html() {
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 9f1eb75..4f92240 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -1,21 +1,17 @@
-# 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
 
-
 import copy
 from urllib.parse import quote
 
 import frappe
 from frappe import _
-from frappe.utils import cint, cstr, nowdate
+from frappe.utils import cint
 from frappe.utils.nestedset import NestedSet
 from frappe.website.utils import clear_cache
 from frappe.website.website_generator import WebsiteGenerator
 
-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.utilities.product import get_qty_in_stock
+from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder
 
 
 class ItemGroup(NestedSet, WebsiteGenerator):
@@ -67,30 +63,11 @@
 		self.delete_child_item_groups_key()
 
 	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.show_search = True
+		context.body_class = "product-page"
+		context.page_length = cint(frappe.db.get_single_value('E Commerce Settings', 'products_per_page')) or 6
 		context.search_link = '/product_search'
 
-		if frappe.form_dict:
-			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)
-		else:
-			search = None
-			attribute_filters = None
-			field_filters = {}
-			start = 0
-
-		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)]
-
-		engine = ProductQuery()
-		context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name)
-
 		filter_engine = ProductFiltersBuilder(self.name)
 
 		context.field_filters = filter_engine.get_field_filters()
@@ -114,15 +91,16 @@
 				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
+		context.name = self.name
+		context.item_group_name = self.item_group_name
 
 		return context
 
@@ -133,91 +111,24 @@
 		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)
+def get_child_groups_for_website(item_group_name, immediate=False):
+	"""Returns child item groups *excluding* passed group."""
+	item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1)
+	filters = {
+		"lft": [">", item_group.lft],
+		"rgt": ["<", item_group.rgt],
+		"show_in_website": 1
+	}
 
-	child_groups = ", ".join(frappe.db.escape(i[0]) for i in get_child_groups(product_group))
+	if immediate:
+		filters["parent_item_group"] = item_group_name
 
-	# 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 frappe.get_all(
+		"Item Group",
+		filters=filters,
+		fields=["name", "route"]
 	)
 
-	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):
-	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})
-
 def get_child_item_groups(item_group_name):
 	item_group = frappe.get_cached_value("Item Group",
 		item_group_name, ["lft", "rgt"], as_dict=1)
@@ -233,31 +144,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": _("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": _(base_nav_page_title), "route":"/"+last_page}
+
 	base_parents = [
-		{"name": frappe._("Home"), "route":"/"},
-		{"name": frappe._("All Products"), "route":"/all-products"},
+		{"name": _("Home"), "route":"/"},
+		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 358b921..74c1bd8 100644
--- a/erpnext/setup/setup_wizard/operations/company_setup.py
+++ b/erpnext/setup/setup_wizard/operations/company_setup.py
@@ -29,10 +29,10 @@
 			'domain': args.get('domains')[0]
 		}).insert()
 
-def enable_shopping_cart(args):
+def enable_shopping_cart(args): # nosemgrep
 	# 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 9dbf49e..cd2738a 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -535,8 +535,8 @@
 			# bank account same as a CoA entry
 			pass
 
-def update_shopping_cart_settings(args):
-	shopping_cart = frappe.get_doc("Shopping Cart Settings")
+def update_shopping_cart_settings(args): # nosemgrep
+	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.js b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js
deleted file mode 100644
index b38828e..0000000
--- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-// License: GNU General Public License v3. See license.txt
-
-frappe.ui.form.on("Shopping Cart 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;
-			frm.refresh_field("quotation_series");
-		}
-
-		frm.set_query('payment_gateway_account', function() {
-			return { 'filters': { 'payment_channel': "Email" } };
-		});
-	},
-	refresh: function(frm) {
-		if (frm.doc.enabled) {
-			frm.get_field('store_page_docs').$wrapper.removeClass('hide-control').html(
-				`<div>${__("Follow these steps to create a landing page for your store")}:
-					<a href="https://docs.erpnext.com/docs/user/manual/en/website/store-landing-page"
-						style="color: var(--gray-600)">
-						docs/store-landing-page
-					</a>
-				</div>`
-			);
-		}
-	},
-	enabled: function(frm) {
-		if (frm.doc.enabled === 1) {
-			frm.set_value('enable_variants', 1);
-		}
-		else {
-			frm.set_value('company', '');
-			frm.set_value('price_list', '');
-			frm.set_value('default_customer_group', '');
-			frm.set_value('quotation_series', '');
-		}
-	}
-});
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 4a75599..0000000
--- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py
+++ /dev/null
@@ -1,84 +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
-
-
-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/filters.py b/erpnext/shopping_cart/filters.py
deleted file mode 100644
index ef0badc..0000000
--- a/erpnext/shopping_cart/filters.py
+++ /dev/null
@@ -1,85 +0,0 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-
-import frappe
-
-
-class ProductFiltersBuilder:
-	def __init__(self, item_group=None):
-		if not item_group or item_group == "Products Settings":
-			self.doc = frappe.get_doc("Products Settings")
-		else:
-			self.doc = frappe.get_doc("Item Group", item_group)
-
-		self.item_group = item_group
-
-	def get_field_filters(self):
-		filter_fields = [row.fieldname for row in self.doc.filter_fields]
-
-		meta = frappe.get_meta('Item')
-		fields = [df for df in meta.fields if df.fieldname in filter_fields]
-
-		filter_data = []
-		for df in fields:
-			filters, or_filters = {}, []
-			if df.fieldtype == "Link":
-				if self.item_group:
-					or_filters.extend([
-						["item_group", "=", self.item_group],
-						["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)
-			else:
-				doctype = df.get_link_doctype()
-
-				# apply enable/disable/show_in_website filter
-				meta = frappe.get_meta(doctype)
-
-				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)]
-
-			# Remove None
-			if None in values:
-				values.remove(None)
-
-			if values:
-				filter_data.append([df, values])
-
-		return filter_data
-
-	def get_attribute_filters(self):
-		attributes = [row.attribute for row in self.doc.filter_attributes]
-
-		if not attributes:
-			return []
-
-		result = frappe.db.sql(
-			"""
-			select
-				distinct attribute, attribute_value
-			from
-				`tabItem Variant Attribute`
-			where
-				attribute in %(attributes)s
-				and attribute_value is not null
-		""",
-			{"attributes": attributes},
-			as_dict=1,
-		)
-
-		attribute_value_map = {}
-		for d in result:
-			attribute_value_map.setdefault(d.attribute, []).append(d.attribute_value)
-
-		out = []
-		for name, values in attribute_value_map.items():
-			out.append(frappe._dict(name=name, item_attribute_values=values))
-		return out
diff --git a/erpnext/shopping_cart/product_info.py b/erpnext/shopping_cart/product_info.py
deleted file mode 100644
index 977f12f..0000000
--- a/erpnext/shopping_cart/product_info.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-
-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 (
-	get_shopping_cart_settings,
-	show_quantity_in_website,
-)
-from erpnext.utilities.product import get_non_stock_item_status, get_price, get_qty_in_stock
-
-
-@frappe.whitelist(allow_guest=True)
-def get_product_info_for_website(item_code, skip_quotation_creation=False):
-	"""get product price / stock info for website"""
-
-	cart_settings = get_shopping_cart_settings()
-	if not cart_settings.enabled:
-		return frappe._dict()
-
-	cart_quotation = frappe._dict()
-	if not skip_quotation_creation:
-		cart_quotation = _get_cart_quotation()
-
-	selling_price_list = cart_quotation.get("selling_price_list") if cart_quotation else _set_price_list(cart_settings, None)
-
-	price = get_price(
-		item_code,
-		selling_price_list,
-		cart_settings.default_customer_group,
-		cart_settings.company
-	)
-
-	stock_status = get_qty_in_stock(item_code, "website_warehouse")
-
-	product_info = {
-		"price": price,
-		"stock_qty": stock_status.stock_qty,
-		"in_stock": stock_status.in_stock if stock_status.is_stock_item else get_non_stock_item_status(item_code, "website_warehouse"),
-		"qty": 0,
-		"uom": frappe.db.get_value("Item", item_code, "stock_uom"),
-		"show_stock_qty": show_quantity_in_website(),
-		"sales_uom": frappe.db.get_value("Item", item_code, "sales_uom")
-	}
-
-	if product_info["price"]:
-		if frappe.session.user != "Guest":
-			item = cart_quotation.get({"item_code": item_code}) if cart_quotation else None
-			if item:
-				product_info["qty"] = item[0].qty
-
-	return frappe._dict({
-		"product_info": product_info,
-		"cart_settings": cart_settings
-	})
-
-def set_product_info_for_website(item):
-	"""set product price uom for website"""
-	product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get("product_info")
-
-	if product_info:
-		item.update(product_info)
-		item["stock_uom"] = product_info.get("uom")
-		item["sales_uom"] = product_info.get("sales_uom")
-		if product_info.get("price"):
-			item["price_stock_uom"] = product_info.get("price").get("formatted_price")
-			item["price_sales_uom"] = product_info.get("price").get("formatted_price_sales_uom")
-		else:
-			item["price_stock_uom"] = ""
-			item["price_sales_uom"] = ""
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 86c702c..2a30ca1 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);
 	}
 });
 
@@ -392,13 +396,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 2d28cc0..e71cdb3 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -117,24 +117,8 @@
   "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",
+  "more_information_section",
+  "published_in_website",
   "total_projected_qty"
  ],
  "fields": [
@@ -857,125 +841,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,
@@ -1017,10 +882,18 @@
    "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
   },
   {
    "default": "1",
@@ -1036,7 +909,6 @@
    "label": "Create Grouped Asset"
   }
  ],
- "has_web_view": 1,
  "icon": "fa fa-tag",
  "idx": 2,
  "image_field": "image",
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index d99fadc..b9e8b3f 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -2,12 +2,12 @@
 # License: GNU General Public License v3. See license.txt
 
 import copy
-import itertools
 import json
 from typing import List
 
 import frappe
 from frappe import _
+from frappe.model.document import Document
 from frappe.utils import (
 	cint,
 	cstr,
@@ -17,13 +17,9 @@
 	getdate,
 	now_datetime,
 	nowtime,
-	random_string,
 	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
 
 import erpnext
 from erpnext.controllers.item_variant import (
@@ -33,10 +29,7 @@
 	make_variant_item_code,
 	validate_item_variant_attributes,
 )
-from erpnext.setup.doctype.item_group.item_group import (
-	get_parent_item_groups,
-	invalidate_cache_for,
-)
+from erpnext.setup.doctype.item_group.item_group import invalidate_cache_for
 from erpnext.stock.doctype.item_default.item_default import ItemDefault
 
 
@@ -51,18 +44,11 @@
 class InvalidBarcode(frappe.ValidationError):
 	pass
 
+class DataValidationError(frappe.ValidationError):
+	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 +89,6 @@
 			self.set_opening_stock()
 
 	def validate(self):
-		super(Item, self).validate()
-
 		if not self.item_name:
 			self.item_name = self.item_code
 
@@ -130,8 +114,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()
@@ -140,21 +122,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.update_variants()
 		self.update_item_price()
-		self.update_template_item()
+		self.update_website_item()
 
 	def validate_description(self):
 		'''Clean HTML description if set'''
@@ -216,97 +194,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):
-		"""Validate if the website image is a public file"""
-
-		if frappe.flags.in_import:
-			return
-
-		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):
-		"""Make a thumbnail of `website_image`"""
-
-		if frappe.flags.in_import:
-			return
-
-		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 +217,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):
 		if not self.is_new() and self.has_value_changed("stock_uom"):
 			self.uoms = []
@@ -507,9 +233,29 @@
 				"conversion_factor": 1
 			})
 
-	def update_show_in_website(self):
-		if self.disabled:
-			self.show_in_website = False
+	def update_website_item(self):
+		"""Update Website Item if change in Item impacts it."""
+		web_item = frappe.db.exists("Website Item", {"item_code": self.item_code})
+
+		if web_item:
+			changed = {}
+			editable_fields = ["item_name", "item_group", "stock_uom", "brand", "description",
+				"disabled"]
+			doc_before_save = self.get_doc_before_save()
+
+			for field in editable_fields:
+				if doc_before_save.get(field) != self.get(field):
+					if field == "disabled":
+						changed["published"] = not self.get(field)
+					else:
+						changed[field] = self.get(field)
+
+			if not changed:
+				return
+
+			web_item_doc = frappe.get_doc("Website Item", web_item)
+			web_item_doc.update(changed)
+			web_item_doc.save()
 
 	def validate_item_tax_net_rate_range(self):
 		for tax in self.get('taxes'):
@@ -641,7 +387,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}):
@@ -652,15 +397,8 @@
 			frappe.db.set_value("Item", old_name, "item_name", new_name)
 
 		if merge:
-			# Validate properties before merging
-			if not frappe.db.exists("Item", new_name):
-				frappe.throw(_("Item {0} does not exist").format(new_name))
-
-			field_list = ["stock_uom", "is_stock_item", "has_serial_no", "has_batch_no"]
-			new_properties = [cstr(d) for d in frappe.db.get_value("Item", new_name, field_list)]
-			if new_properties != [cstr(self.get(fld)) for fld in field_list]:
-				frappe.throw(_("To merge, following properties must be same for both items")
-									+ ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list]))
+			self.validate_properties_before_merge(new_name)
+			self.validate_duplicate_website_item_before_merge(old_name, new_name)
 
 	def after_rename(self, old_name, new_name, merge):
 		if merge:
@@ -668,9 +406,8 @@
 			frappe.msgprint(_("It can take upto few hours for accurate stock values to be visible after merging items."),
 					indicator="orange", title="Note")
 
-		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)
 
@@ -710,7 +447,41 @@
 		msg += _("Note: To merge the items, create a separate Stock Reconciliation for the old item {0}").format(
 			frappe.bold(old_name))
 
-		frappe.throw(_(msg), title=_("Merge not allowed"))
+		frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError)
+
+	def validate_properties_before_merge(self, new_name):
+		# Validate properties before merging
+		if not frappe.db.exists("Item", new_name):
+			frappe.throw(_("Item {0} does not exist").format(new_name))
+
+		field_list = ["stock_uom", "is_stock_item", "has_serial_no", "has_batch_no"]
+		new_properties = [cstr(d) for d in frappe.db.get_value("Item", new_name, field_list)]
+
+		if new_properties != [cstr(self.get(field)) for field in field_list]:
+			msg = _("To merge, following properties must be same for both items")
+			msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])
+			frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
+
+	def validate_duplicate_website_item_before_merge(self, old_name, new_name):
+		"""
+			Block merge if both old and new items have website items against them.
+			This is to avoid duplicate website items after merging.
+		"""
+		web_items = frappe.get_all(
+			"Website Item",
+			filters={
+				"item_code": ["in", [old_name, new_name]]
+			},
+			fields=["item_code", "name"])
+
+		if len(web_items) <= 1:
+			return
+
+		old_web_item = [d.get("name") for d in web_items if d.get("item_code") == old_name][0]
+		web_item_link = get_link_to_form("Website Item", old_web_item)
+
+		msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} and {new_name}"
+		frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError)
 
 	def set_last_purchase_rate(self, new_name):
 		last_purchase_rate = get_last_purchase_details(new_name).get("base_net_rate", 0)
@@ -732,16 +503,6 @@
 
 		frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
 
-	@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
@@ -765,25 +526,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}
 
@@ -1034,47 +776,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({
@@ -1189,14 +890,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)
 
@@ -1204,12 +900,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.variant_selector.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:
@@ -1333,10 +1031,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_item.py b/erpnext/stock/doctype/item/test_item.py
index 0957ce0..fc45ba9 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -536,7 +536,7 @@
 		"check if index is getting created in db"
 
 		indices = frappe.db.sql("show index from tabItem", as_dict=1)
-		expected_columns = {"item_code", "item_name", "item_group", "route"}
+		expected_columns = {"item_code", "item_name", "item_group"}
 		for index in indices:
 			expected_columns.discard(index.get("Column_name"))
 
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 f63498b..be1517e 100644
--- a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py
+++ b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py
@@ -1,4 +1,4 @@
-# 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
 
 
@@ -13,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/doctype/price_list/price_list.py b/erpnext/stock/doctype/price_list/price_list.py
index 74b823a..8a3172e 100644
--- a/erpnext/stock/doctype/price_list/price_list.py
+++ b/erpnext/stock/doctype/price_list/price_list.py
@@ -36,14 +36,14 @@
 			(self.currency, cint(self.buying), cint(self.selling), self.name))
 
 	def check_impact_on_shopping_cart(self):
-		"Check if Price List currency change impacts Shopping Cart."
-		from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
+		"Check if Price List currency change impacts E Commerce Cart."
+		from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
 			validate_cart_settings,
 		)
 
 		doc_before_save = self.get_doc_before_save()
 		currency_changed = self.currency != doc_before_save.currency
-		affects_cart = self.name == frappe.get_cached_value("Shopping Cart Settings", None, "price_list")
+		affects_cart = self.name == frappe.get_cached_value("E Commerce Settings", None, "price_list")
 
 		if currency_changed and affects_cart:
 			validate_cart_settings()
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/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index c60a6ca..81fa045 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -104,6 +104,7 @@
 		{"label": _("Incoming Rate"), "fieldname": "incoming_rate", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency", "convertible": "rate"},
 		{"label": _("Valuation Rate"), "fieldname": "valuation_rate", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency", "convertible": "rate"},
 		{"label": _("Balance Value"), "fieldname": "stock_value", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency"},
+		{"label": _("Value Change"), "fieldname": "stock_value_difference", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency"},
 		{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110},
 		{"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 100},
 		{"label": _("Batch"), "fieldname": "batch_no", "fieldtype": "Link", "options": "Batch", "width": 100},
diff --git a/erpnext/templates/generators/item/item.html b/erpnext/templates/generators/item/item.html
index 17f6880..4070d40 100644
--- a/erpnext/templates/generators/item/item.html
+++ b/erpnext/templates/generators/item/item.html
@@ -1,4 +1,5 @@
 {% extends "templates/web.html" %}
+{% from "erpnext/templates/includes/macros.html" import recommended_item_row %}
 
 {% block title %} {{ title }} {% endblock %}
 
@@ -9,25 +10,70 @@
 {% endblock %}
 
 {% block page_content %}
-<div class="product-container">
+<div class="product-container item-main">
 	{% 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" %}
-
-			{{ doc.website_content or '' }}
 		</div>
 	</div>
 </div>
+
+<!-- Additional Info/Reviews, Recommendations -->
+<div class="d-flex">
+	{% set show_recommended_items = recommended_items and shopping_cart.cart_settings.enable_recommendations %}
+	{% set info_col = 'col-9' if show_recommended_items else 'col-12' %}
+
+	{% set padding_top = 'pt-0' if (show_tabs and tabs) else '' %}
+
+	<div class="product-container mt-4 {{ padding_top }} {{ info_col }}">
+		<div class="item-content {{ 'mt-minus-2' if (show_tabs and tabs) else '' }}">
+			<div class="product-page-content" itemscope itemtype="http://schema.org/Product">
+				<!-- 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>
+
+	<!-- Recommended Items -->
+	{% if show_recommended_items %}
+		<div class="mt-4 col-3 recommended-item-section">
+			<span class="recommendation-header">Recommended</span>
+			<div class="product-container mt-2 recommendation-container">
+				{% for item in recommended_items %}
+					{{ recommended_item_row(item) }}
+				{% endfor %}
+			</div>
+		</div>
+	{% endif %}
+
+</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>
 {{ include_script("frappe-web.bundle.js") }}
 {{ include_script("controls.bundle.js") }}
 {{ include_script("dialog.bundle.js") }}
diff --git a/erpnext/templates/generators/item/item_add_to_cart.html b/erpnext/templates/generators/item/item_add_to_cart.html
index 167c848..8000a24 100644
--- a/erpnext/templates/generators/item/item_add_to_cart.html
+++ b/erpnext/templates/generators/item/item_add_to_cart.html
@@ -5,54 +5,115 @@
 
 <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 %}
-		<div class="product-price">
-			{{ product_info.price.formatted_price_sales_uom }}
-			<small class="formatted-price">({{ product_info.price.formatted_price }} / {{ product_info.uom }})</small>
-		</div>
+			{% set price_info = product_info.price %}
+
+			<div class="product-price">
+				<!-- Final Price -->
+				{{ price_info.formatted_price_sales_uom }}
+
+				<!-- Striked Price and Discount  -->
+				{% if price_info.formatted_mrp %}
+					<small class="formatted-price">
+						<s>MRP {{ price_info.formatted_mrp }}</s>
+					</small>
+					<small class="ml-1 formatted-price in-green">
+						-{{ price_info.get("formatted_discount_percent") or price_info.get("formatted_discount_rate")}}
+					</small>
+				{% endif %}
+
+				<!-- Price per UOM -->
+				<small class="formatted-price ml-2">
+					({{ price_info.formatted_price }} / {{ product_info.uom }})
+				</small>
+			</div>
 		{% else %}
 			{{ _("UOM") }} : {{ product_info.uom }}
 		{% endif %}
 
 		{% if cart_settings.show_stock_availability %}
-		<div>
-			{% if product_info.in_stock == 0 %}
-			<span class="text-danger no-stock">
-				{{ _('Not in stock') }}
-			</span>
+		<div class="mt-2">
+			{% if product_info.get("on_backorder") %}
+				<span class="no-stock out-of-stock" style="color: var(--primary-color);">
+					{{ _('Available on backorder') }}
+				</span>
+			{% elif product_info.in_stock == 0 %}
+				<span class="no-stock out-of-stock">
+					{{ _('Out of stock') }}
+				</span>
 			{% elif product_info.in_stock == 1 %}
-			<span class="text-success has-stock">
-				{{ _('In stock') }}
-				{% if product_info.show_stock_qty and product_info.stock_qty %}
-					({{ product_info.stock_qty[0][0] }})
-				{% endif %}
-			</span>
+				<span class="in-green has-stock">
+					{{ _('In stock') }}
+					{% if product_info.show_stock_qty and product_info.stock_qty %}
+						({{ product_info.stock_qty[0][0] }})
+					{% endif %}
+				</span>
 			{% endif %}
 		</div>
 		{% endif %}
-		<div class="mt-5 mb-5">
-			{% 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 %}"
-					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"
-				>
-					<span class="mr-2">
-						<svg class="icon icon-md">
-							<use href="#icon-assets"></use>
+
+		<!-- Offers -->
+		{% if doc.offers %}
+			<br>
+			<div class="offers-heading mb-4">
+				<span class="mr-1 tag-icon">
+					<svg class="icon icon-lg"><use href="#icon-tag"></use></svg>
+				</span>
+				<b>Available Offers</b>
+			</div>
+			<div class="offer-container">
+				{% for offer in doc.offers %}
+				<div class="mt-2 d-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>
-					</span>
-					{{ _("Add to Cart") }}
-				</button>
-			{% endif %}
-			{% if cart_settings.show_contact_us_button %}
-				{% include "templates/generators/item/item_inquiry.html" %}
-			{% endif %}
+					</div>
+					<p class="mr-1 mb-1">
+						{{ _(offer.offer_title) }}:
+						{{ _(offer.offer_subtitle) if offer.offer_subtitle else '' }}
+						<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-6 mb-5">
+			<div class="mb-4 d-flex">
+				<!-- 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 hidden mr-2 font-md"
+						role="button">
+						{{  _("View in Cart") if cart_settings.enable_checkout else _("View in Quote") }}
+					</a>
+					<button
+						data-item-code="{{item_code}}"
+						class="btn btn-primary btn-add-to-cart mr-2 w-30-40"
+					>
+						<span class="mr-2">
+							<svg class="icon icon-md">
+								<use href="#icon-assets"></use>
+							</svg>
+						</span>
+						{{ _("Add to Cart") if cart_settings.enable_checkout else  _("Add to Quote") }}
+					</button>
+				{% endif %}
+
+				<!-- Contact Us -->
+				{% if cart_settings.show_contact_us_button %}
+					{% include "templates/generators/item/item_inquiry.html" %}
+				{% endif %}
+			</div>
 		</div>
 	</div>
 </div>
@@ -60,10 +121,11 @@
 <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');
-			erpnext.shopping_cart.update_cart({
+			erpnext.e_commerce.shopping_cart.update_cart({
 				item_code,
 				qty: 1,
 				callback(r) {
@@ -74,7 +136,42 @@
 				}
 			});
 		});
+
+		$('.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.html b/erpnext/templates/generators/item/item_configure.html
index b61ac73..e97a275 100644
--- a/erpnext/templates/generators/item/item_configure.html
+++ b/erpnext/templates/generators/item/item_configure.html
@@ -3,11 +3,11 @@
 
 <div class="mt-5 mb-6">
 	{% if cart_settings.enable_variants | int %}
-	<button class="btn btn-primary-light btn-configure"
-		data-item-code="{{ doc.name }}"
-		data-item-name="{{ doc.item_name }}"
+	<button class="btn btn-primary-light btn-configure font-md mr-2"
+		data-item-code="{{ doc.item_code }}"
+		data-item-name="{{ doc.web_item_name }}"
 	>
-		{{ _('Configure') }}
+		{{ _('Select Variant') }}
 	</button>
 	{% endif %}
 	{% if cart_settings.show_contact_us_button %}
diff --git a/erpnext/templates/generators/item/item_configure.js b/erpnext/templates/generators/item/item_configure.js
index 8eadb84..231ae05 100644
--- a/erpnext/templates/generators/item/item_configure.js
+++ b/erpnext/templates/generators/item/item_configure.js
@@ -29,7 +29,7 @@
 		});
 
 		this.dialog = new frappe.ui.Dialog({
-			title: __('Configure {0}', [this.item_name]),
+			title: __('Select Variant for {0}', [this.item_name]),
 			fields,
 			on_hide: () => {
 				set_continue_configuration();
@@ -201,7 +201,7 @@
 				<span class="mr-2">
 					${frappe.utils.icon('assets', 'md')}
 				</span>
-				${__("Add to Cart")}s
+				${__("Add to Cart")}
 			</button>
 		` : '';
 
@@ -214,7 +214,7 @@
 			? `<div class="alert alert-success d-flex justify-content-between align-items-center" role="alert">
 				<div><div>
 					${one_item}
-					${product_info && product_info.price
+					${product_info && product_info.price && !$.isEmptyObject(product_info.price)
 						? '(' + product_info.price.formatted_price_sales_uom + ')'
 						: ''
 					}
@@ -247,7 +247,7 @@
 		const additional_notes = Object.keys(this.range_values || {}).map(attribute => {
 			return `${attribute}: ${this.range_values[attribute]}`;
 		}).join('\n');
-		erpnext.shopping_cart.update_cart({
+		erpnext.e_commerce.shopping_cart.update_cart({
 			item_code,
 			additional_notes,
 			qty: 1
@@ -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.variant_selector.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.variant_selector.utils.get_attributes_and_values', {
 			item_code: this.item_code
 		});
 	}
@@ -311,9 +311,9 @@
 	const { itemCode } = $btn_configure.data();
 
 	if (localStorage.getItem(`configure:${itemCode}`)) {
-		$btn_configure.text(__('Continue Configuration'));
+		$btn_configure.text(__('Continue Selection'));
 	} else {
-		$btn_configure.text(__('Configure'));
+		$btn_configure.text(__('Select Variant'));
 	}
 }
 
diff --git a/erpnext/templates/generators/item/item_details.html b/erpnext/templates/generators/item/item_details.html
index 3b77585..028936b 100644
--- a/erpnext/templates/generators/item/item_details.html
+++ b/erpnext/templates/generators/item/item_details.html
@@ -1,27 +1,63 @@
-<div class="col-md-7 product-details">
-<!-- title -->
-<h1 class="product-title" itemprop="name">
-	{{ item_name }}
-</h1>
-<p class="product-code">
-	<span>{{ _("Item Code") }}:</span>
-	<span itemprop="productID">{{ doc.name }}</span>
-</p>
-{% if has_variants %}
-	<!-- configure template -->
-	{% include "templates/generators/item/item_configure.html" %}
-{% else %}
-	<!-- add variant to cart -->
-	{% include "templates/generators/item/item_add_to_cart.html" %}
-{% endif %}
-<!-- description -->
-<div class="product-description" itemprop="description">
-{% if frappe.utils.strip_html(doc.web_long_description or '') %}
-	{{ doc.web_long_description | safe }}
-{% elif frappe.utils.strip_html(doc.description or '')  %}
-	{{ doc.description | safe }}
-{% else %}
-	{{ _("No description given") }}
-{% endif  %}
+{% set width_class = "expand" if not slides else "" %}
+{% set cart_settings = shopping_cart.cart_settings %}
+{% set product_info = shopping_cart.product_info %}
+{% set price_info = product_info.get('price') or {} %}
+
+<div class="col-md-7 product-details {{ width_class }}">
+	<div class="d-flex">
+		<!-- title -->
+		<div class="product-title col-11" itemprop="name">
+			{{ doc.web_item_name }}
+		</div>
+
+		<!-- Wishlist -->
+		{% if cart_settings.enable_wishlist %}
+			<div class="like-action-item-fp like-action {{ 'like-action-wished' if wished else ''}} ml-2"
+				data-item-code="{{ doc.item_code }}">
+				<svg class="icon sm">
+					<use class="{{ 'wished' if wished else 'not-wished' }} wish-icon" href="#icon-heart"></use>
+				</svg>
+			</div>
+		{% endif %}
+	</div>
+
+	<p class="product-code">
+		<span class="product-item-group">
+			{{ _(doc.item_group) }}
+		</span>
+		<span class="product-item-code">
+			{{ _("Item Code") }}:
+		</span>
+		<span itemprop="productID">{{ doc.item_code }}</span>
+	</p>
+	{% if has_variants %}
+		<!-- configure template -->
+		{% include "templates/generators/item/item_configure.html" %}
+	{% else %}
+		<!-- add variant to cart -->
+		{% include "templates/generators/item/item_add_to_cart.html" %}
+	{% endif %}
+	<!-- description -->
+	<div class="product-description" itemprop="description">
+	{% if frappe.utils.strip_html(doc.web_long_description or '') %}
+		{{ doc.web_long_description | safe }}
+	{% elif frappe.utils.strip_html(doc.description or '')  %}
+		{{ doc.description | safe }}
+	{% else %}
+		{{ "" }}
+	{% endif  %}
+	</div>
 </div>
-</div>
+
+{% block base_scripts %}
+<!-- js should be loaded in body! -->
+<script type="text/javascript" src="/assets/frappe/js/lib/jquery/jquery.min.js"></script>
+{% endblock %}
+
+<script>
+	$('.page_content').on('click', '.like-action-item-fp', (e) => {
+			// Bind action on wishlist button
+			const $btn = $(e.currentTarget);
+			erpnext.e_commerce.wishlist.wishlist_action($btn);
+		});
+</script>
\ No newline at end of file
diff --git a/erpnext/templates/generators/item/item_image.html b/erpnext/templates/generators/item/item_image.html
index 39a30d0..930bb7a 100644
--- a/erpnext/templates/generators/item/item_image.html
+++ b/erpnext/templates/generators/item/item_image.html
@@ -1,29 +1,30 @@
-<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 %}
-		<img class="item-slideshow-image mb-2 {% if loop.first %}active{% endif %}"
-				src="{{ item.image }}" alt="{{ item.heading }}">
-		{% endfor %}
-	</div>
-	{{ product_image(slides[0].image, 'product-image') }}
-	<!-- Simple image slideshow -->
-	<script>
-		frappe.ready(() => {
-			$('.page_content').on('click', '.item-slideshow-image', (e) => {
-				const $img = $(e.currentTarget);
-				const link = $img.prop('src');
-				const $product_image = $('.product-image');
-				$product_image.find('a').prop('href', link);
-				$product_image.find('img').prop('src', link);
+		<div class="item-slideshow d-flex flex-column mr-3">
+			{% for item in slides %}
+			<img class="item-slideshow-image mb-2 {% if loop.first %}active{% endif %}"
+					src="{{ item.image }}" alt="{{ item.heading }}">
+			{% endfor %}
+		</div>
+		{{ product_image(slides[0].image, 'product-image') }}
+		<!-- Simple image slideshow -->
+		<script>
+			frappe.ready(() => {
+				$('.page_content').on('click', '.item-slideshow-image', (e) => {
+					const $img = $(e.currentTarget);
+					const link = $img.prop('src');
+					const $product_image = $('.product-image');
+					$product_image.find('a').prop('href', link);
+					$product_image.find('img').prop('src', link);
 
-				$('.item-slideshow-image').removeClass('active');
-				$img.addClass('active');
-			});
-		})
-	</script>
+					$('.item-slideshow-image').removeClass('active');
+					$img.addClass('active');
+				});
+			})
+		</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, alt=doc.website_image_alt or doc.item_name) }}
 	{% endif %}
 
 	<!-- Simple image preview -->
diff --git a/erpnext/templates/generators/item/item_inquiry.html b/erpnext/templates/generators/item/item_inquiry.html
index 83653b6..af636f1 100644
--- a/erpnext/templates/generators/item/item_inquiry.html
+++ b/erpnext/templates/generators/item/item_inquiry.html
@@ -1,9 +1,9 @@
 {% if shopping_cart and shopping_cart.cart_settings.enabled %}
 {% set cart_settings = shopping_cart.cart_settings %}
-    {% if cart_settings.show_contact_us_button | int %}
-        <button class="btn btn-inquiry btn-primary-light" data-item-code="{{ doc.name }}">
-            {{ _('Contact Us') }}
-        </button>
+	{% if cart_settings.show_contact_us_button | int %}
+		<button class="btn btn-inquiry font-md w-30-40" data-item-code="{{ doc.name }}">
+			{{ _('Contact Us') }}
+		</button>
 	{% endif %}
 <script>
 {% include "templates/generators/item/item_inquiry.js" %}
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..c62c6f7
--- /dev/null
+++ b/erpnext/templates/generators/item/item_reviews.html
@@ -0,0 +1,88 @@
+{% from "erpnext/templates/includes/macros.html" import user_review, ratings_summary %}
+
+<div class="mt-4 ratings-reviews-section">
+		<!-- Title and Action -->
+		<div class="w-100 mt-4 mb-2 d-flex">
+			<div class="reviews-header col-9">
+				{{ _("Customer Reviews") }}
+			</div>
+
+			<div class="write-a-review-btn col-3">
+				<!-- Write a Review for legitimate users -->
+				{% if frappe.session.user != "Guest" and user_is_customer %}
+					<button class="btn btn-write-review"
+						data-web-item="{{ doc.name }}">
+						{{ _("Write a Review") }}
+					</button>
+				{% endif %}
+			</div>
+		</div>
+
+		<!-- Summary -->
+		{{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating, for_summary=True, total_reviews=total_reviews) }}
+
+
+	<!-- Reviews and Comments -->
+	<div class="mt-8">
+		{% if reviews %}
+			{{ user_review(reviews) }}
+
+			{% if total_reviews > 4 %}
+				<div class="mt-6 mb-6"style="color: var(--primary);">
+					<a href="/customer_reviews?web_item={{ doc.name }}">{{ _("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 the 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..0814d81 100644
--- a/erpnext/templates/generators/item/item_specifications.html
+++ b/erpnext/templates/generators/item/item_specifications.html
@@ -1,14 +1,20 @@
-{% 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-2' if not show_tabs else 'mt-5'}} item-website-specification">
+	<div class="col-md-11">
+		{% if not show_tabs %}
+			<div class="product-title mb-5 mt-4">
+				Product Details
+			</div>
+		{% endif %}
+		<table class="table">
+		{% for d in website_specifications -%}
 			<tr>
-				<td class="text-muted" style="width: 30%;">{{ d.label }}</td>
-				<td>{{ d.description }}</td>
+				<td class="spec-label">{{ d.label }}</td>
+				<td class="spec-content">{{ 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..e099cdd 100644
--- a/erpnext/templates/generators/item_group.html
+++ b/erpnext/templates/generators/item_group.html
@@ -1,17 +1,25 @@
+{% from "erpnext/templates/includes/macros.html" import field_filter_section, attribute_filter_section, discount_range_filters %}
 {% extends "templates/web.html" %}
 
 {% block header %}
-<!-- <h2>{{ title }}</h2> -->
+<div class="mb-6">{{ _(item_group_name) }}</div>
 {% endblock header %}
 
 {% block script %}
 <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-content" itemscope itemtype="http://schema.org/Product"
+	data-item-group="{{ name }}">
 	<div class="item-group-slideshow">
-		{% if slideshow %}<!-- slideshow -->
+		{% if slideshow %} <!-- slideshow -->
 			{{ web_block(
 				"Hero Slider",
 				values=slideshow,
@@ -20,91 +28,28 @@
 				add_bottom_padding=0,
 			) }}
 		{% endif %}
-		<h2 class="mt-3">{{ title }}</h2>
-		{% if description %}<!-- description -->
+
+		{% if description %} <!-- description -->
 		<div class="item-group-description text-muted mb-5" itemprop="description">{{ description or ""}}</div>
 		{% endif %}
 	</div>
 	<div class="row">
-		<div class="col-12 order-2 col-md-9 order-md-2 item-card-group-section">
-			<div class="row products-list">
-				{% if items %}
-					{% for item in items %}
-						{% include "erpnext/www/all-products/item_row.html" %}
-					{% endfor %}
-				{% else %}
-					{% include "erpnext/www/all-products/not_found.html" %}
-				{% endif %}
-			</div>
+		<div id="product-listing" class="col-12 order-2 col-md-9 order-md-2 item-card-group-section">
+			<!-- Products Rendered in all-products/index.js-->
 		</div>
+
 		<div class="col-12 order-1 col-md-3 order-md-1">
 			<div class="collapse d-md-block mr-4 filters-section" id="product-filters">
 				<div class="d-flex justify-content-between align-items-center mb-5 title-section">
 					<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 %}
 			</div>
 
 			<script>
@@ -127,23 +72,6 @@
 			</script>
 		</div>
 	</div>
-	<div class="row mt-6">
-		<div class="col-3">
-		</div>
-		<div class="col-9">
-			{% if frappe.form_dict.start|int > 0 %}
-			<button class="btn btn-outline-secondary btn-prev" data-start="{{ frappe.form_dict.start|int - page_length }}">
-				{{ _("Prev") }}
-			</button>
-			{% endif %}
-			{% if items|length >= page_length %}
-			<button class="btn btn-outline-secondary btn-next" data-start="{{ frappe.form_dict.start|int + page_length }}"
-				style="float: right;">
-				{{ _("Next") }}
-			</button>
-			{% endif %}
-		</div>
-	</div>
 </div>
 
 <script>
diff --git a/erpnext/templates/includes/cart/address_card.html b/erpnext/templates/includes/cart/address_card.html
index 667144b..830ed64 100644
--- a/erpnext/templates/includes/cart/address_card.html
+++ b/erpnext/templates/includes/cart/address_card.html
@@ -1,5 +1,5 @@
 <div class="card address-card h-100">
-	<div class="btn btn-sm btn-default btn-change-address" style="position: absolute; right: 0; top: 0;">
+	<div class="btn btn-sm btn-default btn-change-address font-md" style="position: absolute; right: 0; top: 0;">
 		{{ _('Change') }}
 	</div>
 	<div class="card-body p-0">
diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html
index 4482bc1..cf60017 100644
--- a/erpnext/templates/includes/cart/cart_address.html
+++ b/erpnext/templates/includes/cart/cart_address.html
@@ -4,18 +4,14 @@
 	{% set select_address = True %}
 {% endif %}
 
-{% set show_coupon_code = frappe.db.get_single_value('Shopping Cart Settings', 'show_apply_coupon_code_in_website') %}
-{% if show_coupon_code == 1%}
-<div class="mb-3">
-	<div class="row no-gutters">
-		<input type="text" class="txtcoupon form-control mr-3 w-25" placeholder="Enter Coupon Code" name="txtcouponcode"  ></input>
-		<button class="btn btn-primary btn-sm  bt-coupon">{{ _("Apply Coupon Code") }}</button>
-		<input type="hidden" class="txtreferral_sales_partner" placeholder="Enter Sales Partner" name="txtreferral_sales_partner" type="text"></input>
-		</div>
-</div>
-{% endif %}
 <div class="mb-3 frappe-card p-5" data-section="shipping-address">
-	<h6>{{ _("Shipping Address") }}</h6>
+	<div class="d-flex">
+		<div class="col-6 address-header"><h6>{{ _("Shipping Address") }}</h6></div>
+		<div class="col-6" style="padding: 0;">
+			<a class="ml-4 btn-new-address" role="button">{{ _("Add a new address") }}</a>
+		</div>
+	</div>
+
 	<hr>
 	{% for address in shipping_addresses %}
 	{% if doc.shipping_address_name == address.name %}
@@ -27,26 +23,36 @@
 	{% endif %}
 	{% endfor %}
 </div>
+
+<!-- Billing Address -->
 <div class="checkbox ml-1 mb-2">
 	<label for="input_same_billing">
-		<input type="checkbox" id="input_same_billing" checked>
-		<span class="label-area">{{ _('Billing Address is same as Shipping Address') }}</span>
+		<input type="checkbox" class="product-filter" id="input_same_billing" checked style="width: 14px !important">
+		<span class="label-area font-md">{{ _('Billing Address is same as Shipping Address') }}</span>
 	</label>
 </div>
-<div class="mb-3 frappe-card p-5" data-section="billing-address">
-	<h6>{{ _("Billing Address") }}</h6>
-	<hr>
-	{% for address in billing_addresses %}
-		{% if doc.customer_address == address.name %}
-		<div class="row no-gutters" data-fieldname="customer_address">
-			<div class="w-100 address-container" data-address-name="{{address.name}}" data-address-type="billing" data-active>
-					{% include "templates/includes/cart/address_card.html" %}
-				</div>
+
+{% if billing_addresses %}
+	<div class="mb-3 frappe-card p-5" data-section="billing-address">
+		<div class="d-flex">
+			<div class="col-6 address-header"><h6>{{ _("Billing Address") }}</h6></div>
+			<div class="col-6" style="padding: 0;">
+				<a class="ml-4 btn-new-address" role="button">{{ _("Add a new address") }}</a>
+			</div>
 		</div>
-		{% endif %}
-	{% endfor %}
-</div>
-<button class="btn btn-outline-primary btn-sm mt-1 btn-new-address bg-white">{{ _("Add a new address") }}</button>
+
+		<hr>
+		{% for address in billing_addresses %}
+			{% if doc.customer_address == address.name %}
+			<div class="row no-gutters" data-fieldname="customer_address">
+				<div class="w-100 address-container" data-address-name="{{address.name}}" data-address-type="billing" data-active>
+						{% include "templates/includes/cart/address_card.html" %}
+					</div>
+			</div>
+			{% endif %}
+		{% endfor %}
+	</div>
+{% endif %}
 
 <script>
 frappe.ready(() => {
@@ -125,15 +131,16 @@
 				{
 					fieldname: "phone",
 					fieldtype: "Data",
-					label: "Phone"
+					label: "Phone",
+					reqd: 1
 				},
 			],
 			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/cart/cart_items.html b/erpnext/templates/includes/cart/cart_items.html
index 75441c4..428b36e 100644
--- a/erpnext/templates/includes/cart/cart_items.html
+++ b/erpnext/templates/includes/cart/cart_items.html
@@ -1,42 +1,113 @@
-{% for d in doc.items %}
-<tr data-name="{{ d.name }}">
-	<td>
-		<div class="item-title mb-1">
-			{{ d.item_name }}
-		</div>
-		<div class="item-subtitle">
-			{{ d.item_code }}
-		</div>
-		{%- set variant_of = frappe.db.get_value('Item', d.item_code, 'variant_of') %}
-		{% if variant_of %}
-		<span class="item-subtitle">
-			{{ _('Variant of') }} <a href="{{frappe.db.get_value('Item', variant_of, 'route')}}">{{ variant_of }}</a>
-		</span>
-		{% endif %}
-		<div class="mt-2">
-			<textarea data-item-code="{{d.item_code}}" class="form-control" rows="2" placeholder="{{ _('Add notes') }}">{{d.additional_notes or ''}}</textarea>
-		</div>
-	</td>
-	<td class="text-right">
-		<div class="input-group number-spinner">
-			<span class="input-group-prepend d-none d-sm-inline-block">
-				<button class="btn cart-btn" data-dir="dwn">–</button>
-			</span>
-			<input class="form-control text-center cart-qty" value="{{ d.get_formatted('qty') }}" data-item-code="{{ d.item_code }}">
-			<span class="input-group-append d-none d-sm-inline-block">
-				<button class="btn cart-btn" data-dir="up">+</button>
+{% from "erpnext/templates/includes/macros.html" import product_image %}
+
+{% macro item_subtotal(item) %}
+	<div>
+		{{ item.get_formatted('amount') }}
+	</div>
+
+	{% if item.is_free_item %}
+		<div class="text-success mt-4">
+			<span class="free-tag">
+				{{ _('FREE') }}
 			</span>
 		</div>
-	</td>
-	{% if cart_settings.enable_checkout %}
-	<td class="text-right item-subtotal">
-		<div>
-			{{ d.get_formatted('amount') }}
-		</div>
+	{% else %}
 		<span class="item-rate">
-			{{ _('Rate:') }} {{ d.get_formatted('rate') }}
+			{{ _('Rate:') }} {{ item.get_formatted('rate') }}
 		</span>
-	</td>
 	{% endif %}
-</tr>
+{% endmacro %}
+
+{% for d in doc.items %}
+	<tr data-name="{{ d.name }}">
+		<td style="width: 60%;">
+			<div class="d-flex">
+				<div class="cart-item-image mr-4">
+					{% if d.thumbnail %}
+						{{ product_image(d.thumbnail, alt="d.web_item_name", no_border=True) }}
+					{% else %}
+						<div class = "no-image-cart-item">
+							{{ frappe.utils.get_abbr(d.web_item_name) or "NA" }}
+						</div>
+					{% endif %}
+				</div>
+
+				<div class="d-flex w-100" style="flex-direction: column;">
+					<div class="item-title mb-1 mr-3">
+						{{ d.get("web_item_name") or d.item_name }}
+					</div>
+					<div class="item-subtitle mr-2">
+						{{ d.item_code }}
+					</div>
+					{%- set variant_of = frappe.db.get_value('Item', d.item_code, 'variant_of') %}
+					{% if variant_of %}
+					<span class="item-subtitle mr-2">
+						{{ _('Variant of') }}
+						<a href="{{frappe.db.get_value('Website Item', {'item_code': variant_of}, 'route') or '#'}}">
+							{{ variant_of }}
+						</a>
+					</span>
+					{% endif %}
+
+					<div class="mt-2 notes">
+						<textarea data-item-code="{{d.item_code}}" class="form-control" rows="2" placeholder="{{ _('Add notes') }}">
+							{{d.additional_notes or ''}}
+						</textarea>
+					</div>
+				</div>
+			</div>
+		</td>
+
+		<!-- Qty column -->
+		<td class="text-right" style="width: 25%;">
+			<div class="d-flex">
+				{% set disabled = 'disabled' if d.is_free_item else '' %}
+				<div class="input-group number-spinner mt-1 mb-4">
+					<span class="input-group-prepend d-sm-inline-block">
+						<button class="btn cart-btn" data-dir="dwn" {{ disabled }}>
+							{{ '–' if not d.is_free_item else ''}}
+						</button>
+					</span>
+
+					<input class="form-control text-center cart-qty" value="{{ d.get_formatted('qty') }}" data-item-code="{{ d.item_code }}"
+						style="max-width: 70px;" {{ disabled }}>
+
+					<span class="input-group-append d-sm-inline-block">
+						<button class="btn cart-btn" data-dir="up" {{ disabled }}>
+							{{ '+' if not d.is_free_item else ''}}
+						</button>
+					</span>
+					</div>
+
+				<div>
+					{% if not d.is_free_item %}
+						<div class="remove-cart-item column-sm-view d-flex" data-item-code="{{ d.item_code }}">
+							<span>
+								<svg class="icon sm remove-cart-item-logo"
+									width="18" height="18" viewBox="0 0 18 18"
+									xmlns="http://www.w3.org/2000/svg" id="icon-close">
+									<path fill-rule="evenodd" clip-rule="evenodd" d="M4.146 11.217a.5.5 0 1 0 .708.708l3.182-3.182 3.181 3.182a.5.5 0 1 0 .708-.708l-3.182-3.18 3.182-3.182a.5.5 0 1 0-.708-.708l-3.18 3.181-3.183-3.182a.5.5 0 0 0-.708.708l3.182 3.182-3.182 3.181z" stroke-width="0"></path>
+								</svg>
+							</span>
+						</div>
+					{% endif %}
+					</div>
+			</div>
+
+
+			<!-- Shown on mobile view, else hidden -->
+			{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}
+				<div class="text-right sm-item-subtotal">
+					{{ item_subtotal(d) }}
+				</div>
+			{% endif %}
+		</td>
+
+		<!-- Subtotal column -->
+		{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}
+			<td class="text-right item-subtotal column-sm-view w-100">
+				{{ item_subtotal(d) }}
+			</td>
+		{% endif %}
+	</tr>
 {% endfor %}
diff --git a/erpnext/templates/includes/cart/cart_items_total.html b/erpnext/templates/includes/cart/cart_items_total.html
new file mode 100644
index 0000000..c94fde4
--- /dev/null
+++ b/erpnext/templates/includes/cart/cart_items_total.html
@@ -0,0 +1,10 @@
+<!-- Total at the end of the cart items -->
+<tr>
+	<th></th>
+	<th class="text-left item-grand-total" colspan="1">
+		{{ _("Total") }}
+	</th>
+	<th class="text-left item-grand-total totals" colspan="3">
+		{{ doc.get_formatted("total") }}
+	</th>
+</tr>
\ No newline at end of file
diff --git a/erpnext/templates/includes/cart/cart_payment_summary.html b/erpnext/templates/includes/cart/cart_payment_summary.html
new file mode 100644
index 0000000..b5655a2
--- /dev/null
+++ b/erpnext/templates/includes/cart/cart_payment_summary.html
@@ -0,0 +1,84 @@
+<!-- Payment -->
+{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}
+<h6>
+	{{ _("Payment Summary") }}
+</h6>
+{% endif %}
+
+<div class="card h-100">
+	<div class="card-body p-0">
+		{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}
+			<table class="table w-100">
+				<tr>
+					{% set total_items = frappe.utils.cstr(frappe.utils.flt(doc.total_qty, 0)) %}
+					<td class="bill-label">{{ _("Net Total (") + total_items + _(" Items)") }}</td>
+					<td class="bill-content net-total text-right">{{ doc.get_formatted("net_total") }}</td>
+				</tr>
+
+				<!-- taxes -->
+				{% for d in doc.taxes %}
+					{% if d.base_tax_amount %}
+						<tr>
+							<td class="bill-label">
+								{{ d.description }}
+							</td>
+							<td class="bill-content text-right">
+								{{ d.get_formatted("base_tax_amount") }}
+							</td>
+						</tr>
+					{% endif %}
+				{% endfor %}
+			</table>
+
+			<!-- TODO: Apply Coupon Dialog-->
+			<!-- {% set show_coupon_code = cart_settings.show_apply_coupon_code_in_website and cart_settings.enable_checkout %}
+			{% if show_coupon_code %}
+				<button class="btn btn-coupon-code w-100 text-left">
+					<svg width="24" height="24" viewBox="0 0 24 24" stroke="var(--gray-600)" 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>
+					<span class="ml-2">Apply Coupon</span>
+				</button>
+			{% endif %} -->
+
+			<table class="table w-100 grand-total mt-6">
+				<tr>
+					<td class="bill-content net-total">{{ _("Grand Total") }}</td>
+					<td class="bill-content net-total text-right">{{ doc.get_formatted("grand_total") }}</td>
+				</tr>
+			</table>
+		{% endif %}
+
+		{% if cart_settings.enable_checkout %}
+			<button class="btn btn-primary btn-place-order font-md w-100" type="button">
+				{{ _('Place Order') }}
+			</button>
+		{% else %}
+			<button class="btn btn-primary btn-request-for-quotation font-md w-100" type="button">
+				{{ _('Request for Quote') }}
+			</button>
+		{% endif %}
+	</div>
+</div>
+
+<!-- TODO: Apply Coupon Dialog-->
+<!-- <script>
+	frappe.ready(() => {
+		$('.btn-coupon-code').click((e) => {
+			const $btn = $(e.currentTarget);
+			const d = new frappe.ui.Dialog({
+				title: __('Coupons'),
+				fields: [
+					{
+						fieldname: 'coupons_area',
+						fieldtype: 'HTML'
+					}
+				]
+			});
+			d.show();
+		});
+	});
+</script> -->
\ No newline at end of file
diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html
index be0d47f..4741307 100644
--- a/erpnext/templates/includes/macros.html
+++ b/erpnext/templates/includes/macros.html
@@ -7,9 +7,15 @@
 </div>
 {% endmacro %}
 
-{% macro product_image(website_image, css_class="product-image", alt="") %}
-	<div class="border text-center rounded {{ css_class }}" style="overflow: hidden;">
-		<img itemprop="image" class="website-image h-100 w-100" alt="{{ alt }}" src="{{ frappe.utils.quoted(website_image or 'no-image.jpg') | abs_url }}">
+{% macro product_image(website_image, css_class="product-image", alt="", no_border=False) %}
+	<div class="{{ 'border' if not no_border else ''}} text-center rounded {{ css_class }}" style="overflow: hidden;">
+		{% if website_image %}
+			<img itemprop="image" class="website-image h-100 w-100" alt="{{ alt }}" src="{{ frappe.utils.quoted(website_image) | abs_url }}">
+		{% else %}
+			<div class="card-img-top no-image-item">
+				{{ frappe.utils.get_abbr(alt) or "NA" }}
+			</div>
+		{% endif %}
 	</div>
 {% endmacro %}
 
@@ -59,65 +65,335 @@
 
 {% endmacro %}
 
-{%- macro item_card(title, image, url, description, rate, category, is_featured=False, is_full_width=False, align="Left") -%}
+{%- macro item_card(item, 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.web_item_name or item.item_name or item.item_code -%}
+{%- set title = title[:50] + "..." if title|len > 50 else title -%}
+{%- 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 }}">
+	<div class="card featured-item {{ align_items_class }}" style="height: 360px;">
 		{% if image %}
 		<div class="row no-gutters">
-			<div class="col-md-6">
+			<div class="col-md-5 ml-4">
 				<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, 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, description, item, is_featured, align) }}
 			</div>
 		{% endif %}
 	</div>
 </div>
 {% else %}
 <div class="col-sm-{{ col_size }} item-card">
-	<div class="card {{ align_items_class }}">
+	<div class="card {{ align_items_class }}" style="height: 360px;">
 		{% 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, 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, 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 class="mt-4">
+		<a href="/{{ item.route or '#' }}">
+			<div class="product-title">
+				{{ title or '' }}
+			</div>
+		</a>
+	</div>
 	{% if is_featured %}
-	<div class="product-price">{{ rate or '' }}</div>
-	<div class="product-description ellipsis">{{ description or '' }}</div>
+		<div class="product-description ellipsis text-muted" style="white-space: normal;">
+			{{ 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>
 	{% endif %}
 </div>
-<a href="/{{ url or '#' }}" class="stretched-link"></a>
+{%- endmacro -%}
+
+
+{%- macro wishlist_card(item, settings) %}
+{%- set title = item.web_item_name or ''-%}
+{%- set title = title[:90] + "..." if title|len > 90 else title -%}
+<div class="col-sm-3 wishlist-card">
+	<div class="card text-center">
+		<div class="card-img-container">
+			<a href="/{{ item.route or '#' }}" style="text-decoration: none;">
+				{% if item.image %}
+					<img class="card-img" src="{{ item.image }}" alt="{{ title }}">
+				{% else %}
+					<div class="card-img-top no-image">
+						{{ frappe.utils.get_abbr(title) }}
+					</div>
+				{% endif %}
+			</a>
+			<div class="remove-wish" data-item-code="{{ item.item_code }}">
+				<svg class="icon icon-md remove-wish-icon">
+					<use class="close" href="#icon-delete"></use>
+				</svg>
+			</div>
+		</div>
+
+		{{ wishlist_card_body(item, title, settings) }}
+	</div>
+</div>
+{%- endmacro -%}
+
+{%- macro wishlist_card_body(item, title, settings) %}
+<div class="card-body card-body-flex text-left" style="width: 100%;">
+	<div class="mt-4">
+		<div class="product-title">{{ title or ''}}</div>
+		<div class="product-category">{{ item.item_group or '' }}</div>
+	</div>
+	<div class="product-price">
+		{{ item.get("formatted_price") or '' }}
+
+		{% if item.get("formatted_mrp") %}
+			<small class="ml-1 striked-price">
+				<s>{{ item.formatted_mrp }}</s>
+			</small>
+			<small class="ml-1 product-info-green" >
+				{{ 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-primary btn-add-to-cart-list btn-add-to-cart mt-2 w-100">
+			<span class="mr-2">
+				<svg class="icon icon-md">
+					<use href="#icon-assets"></use>
+				</svg>
+			</span>
+			{{ _("Move to Cart") }}
+		</button>
+	{% else %}
+		<div class="out-of-stock">
+			{{ _("Out of stock") }}
+		</div>
+	{% endif %}
+</div>
+{%- endmacro -%}
+
+{%- macro ratings_with_title(avg_rating, title, size, rating_header_class, for_summary=False) -%}
+<div class="{{ 'd-flex' if not for_summary else '' }}">
+	<p class="mr-4 {{ rating_header_class }}">
+		<span>{{ title }}</span>
+	</p>
+	<div class="rating {{ 'ratings-pill' if for_summary else ''}}">
+		{% 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>
+</div>
+{%- endmacro -%}
+
+{%- macro ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating, for_summary=False, total_reviews=None)-%}
+<div class="rating-summary-section mt-4">
+	<div class="rating-summary-numbers col-3">
+		<h2 style="font-size: 2rem;">
+			{{ average_rating or 0 }}
+		</h2>
+		<div class="mb-2" style="margin-top: -.5rem;">
+			{{ frappe.utils.cstr(total_reviews or 0) + " " + _("ratings") }}
+		</div>
+
+		<!-- Ratings Summary -->
+		{% if reviews %}
+			{% set rating_title = frappe.utils.cstr(average_rating) + " " + _("out of 5") if not for_summary else ''%}
+			{{ ratings_with_title(average_whole_rating, rating_title, "md", "rating-summary-title", for_summary) }}
+		{% endif %}
+
+		<div class="mt-2">{{ frappe.utils.cstr(average_rating or 0) + " " + _("out of 5") }}</div>
+	</div>
+
+	<!-- Rating Progress Bars -->
+	<div class="rating-progress-bar-section col-4 ml-4">
+		{% for percent in reviews_per_rating %}
+			<div class="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 progress-bar-cosmetic" role="progressbar"
+							aria-valuenow="{{ percent }}"
+							aria-valuemin="0" aria-valuemax="100"
+							style="width: {{ percent }}%;">
+						</div>
+					</div>
+				</div>
+				<div class="col-sm-1 small">
+					{{ percent }}%
+				</div>
+			</div>
+		{% endfor %}
+	</div>
+</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), "sm", "user-review-title") }}
+
+			<div class="product-description mb-4">
+				<p>
+					{{ _(review.comment) }}
+				</p>
+			</div>
+
+			<div class="review-signature mb-2">
+				<span class="reviewer">{{ _(review.customer) }}</span>
+				<span class="indicator grey" style="--text-on-gray: var(--gray-300);"></span>
+				<span class="reviewer">{{ review.published_on }}</span>
+			</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 }}"
+						style="width: 14px !important">
+					<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="{{ attr_value }}">
+					<input type="checkbox"
+						class="product-filter attribute-filter"
+						id="{{ attr_value }}"
+						data-attribute-name="{{ attribute.name }}"
+						data-attribute-value="{{ attr_value }}"
+						style="width: 14px !important"
+						{% 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 %}
+{%- endmacro -%}
+
+{%- macro recommended_item_row(item)-%}
+<div class="recommended-item mb-6 d-flex">
+	<div class="r-item-image">
+		{% if item.website_item_thumbnail %}
+			{{ product_image(item.website_item_thumbnail, css_class="r-product-image", alt="item.website_item_name", no_border=True) }}
+		{% else %}
+			<div class="no-image-r-item">
+				{{ frappe.utils.get_abbr(item.website_item_name) or "NA" }}
+			</div>
+		{% endif %}
+	</div>
+	<div class="r-item-info">
+		<a href="/{{ item.route or '#'}}" target="_blank">
+			{% set title = item.website_item_name %}
+			{{ title[:70] + "..." if title|len > 70 else title }}
+		</a>
+
+		{% if item.get('price_info') %}
+			{% set price = item.get('price_info') %}
+			<div class="mt-2">
+				<span class="item-price">
+					{{ price.get('formatted_price') or '' }}
+				</span>
+
+				{% if price.get('formatted_mrp') %}
+					<br>
+					<span class="striked-item-price">
+						<s>MRP {{ price.formatted_mrp }}</s>
+					</span>
+					<span class="in-green">
+						- {{ price.get('formatted_discount_percent') or price.get('formatted_discount_rate')}}
+					</span>
+				{% endif %}
+			</div>
+		{% endif %}
+	</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/order/order_macros.html b/erpnext/templates/includes/order/order_macros.html
index 7b3c9a4..3f2c1f2 100644
--- a/erpnext/templates/includes/order/order_macros.html
+++ b/erpnext/templates/includes/order/order_macros.html
@@ -1,43 +1,49 @@
-{% from "erpnext/templates/includes/macros.html" import product_image_square %}
+{% from "erpnext/templates/includes/macros.html" import product_image %}
 
 {% macro item_name_and_description(d) %}
-    <div class="row item_name_and_description">
-        <div class="col-xs-4 col-sm-2 order-image-col">
-            <div class="order-image">
-                {{ product_image_square(d.thumbnail or d.image) }}
-            </div>
-        </div>
-        <div class="col-xs-8 col-sm-10">
-            {{ d.item_code }}
-            <div class="text-muted small item-description">
+	<div class="row item_name_and_description">
+		<div class="col-xs-4 col-sm-2 order-image-col">
+			<div class="order-image">
+				{% if d.thumbnail or d.image %}
+					{{ product_image(d.thumbnail or d.image, no_border=True) }}
+				{% else %}
+					<div class="no-image-cart-item" style="min-height: 100px;">
+						{{ frappe.utils.get_abbr(d.item_name) or "NA" }}
+					</div>
+				{% endif %}
+			</div>
+		</div>
+		<div class="col-xs-8 col-sm-10">
+			{{ d.item_code }}
+			<div class="text-muted small item-description">
 				{{ html2text(d.description) | truncate(140) }}
 			</div>
-        </div>
-    </div>
+		</div>
+	</div>
 {% endmacro %}
 
 {% macro item_name_and_description_cart(d) %}
-    <div class="row item_name_dropdown">
-        <div class="col-xs-4 col-sm-4 order-image-col">
-            <div class="order-image">
-             {{ product_image_square(d.thumbnail or d.image) }}
-            </div>
-        </div>
-        <div class="col-xs-8 col-sm-8">
-           {{ d.item_name|truncate(25) }}
-			<div class="input-group number-spinner">
-                <span class="input-group-btn">
-                    <button class="btn btn-light cart-btn" data-dir="dwn">
-                        –</button>
-                </span>
-	            <input class="form-control text-right cart-qty"
-		            value = "{{ d.get_formatted('qty') }}"
-		            data-item-code="{{ d.item_code }}">
-                <span class="input-group-btn">
-                    <button class="btn btn-light cart-btn" data-dir="up">
-                        +</button>
-                </span>
+	<div class="row item_name_dropdown">
+		<div class="col-xs-4 col-sm-4 order-image-col">
+			<div class="order-image">
+			 {{ product_image_square(d.thumbnail or d.image) }}
 			</div>
-        </div>
-    </div>
+		</div>
+		<div class="col-xs-8 col-sm-8">
+		   {{ d.item_name|truncate(25) }}
+			<div class="input-group number-spinner">
+				<span class="input-group-btn">
+					<button class="btn btn-light cart-btn" data-dir="dwn">
+						–</button>
+				</span>
+				<input class="form-control text-right cart-qty"
+					value = "{{ d.get_formatted('qty') }}"
+					data-item-code="{{ d.item_code }}">
+				<span class="input-group-btn">
+					<button class="btn btn-light cart-btn" data-dir="up">
+						+</button>
+				</span>
+			</div>
+		</div>
+	</div>
 {% endmacro %}
diff --git a/erpnext/templates/includes/order/order_taxes.html b/erpnext/templates/includes/order/order_taxes.html
index d2c458e..b821e62 100644
--- a/erpnext/templates/includes/order/order_taxes.html
+++ b/erpnext/templates/includes/order/order_taxes.html
@@ -1,9 +1,9 @@
 {% if doc.taxes %}
 <tr>
-	<td class="text-right" colspan="2">
+	<td class="text-left" colspan="1">
 		{{ _("Net Total") }}
 	</td>
-	<td class="text-right">
+	<td class="text-right totals" colspan="3">
 		{{ doc.get_formatted("net_total") }}
 	</td>
 </tr>
@@ -12,10 +12,10 @@
 {% for d in doc.taxes %}
 	{% if d.base_tax_amount %}
 	<tr>
-		<td class="text-right" colspan="2">
+		<td class="text-left" colspan="1">
 			{{ d.description }}
 		</td>
-		<td class="text-right">
+		<td class="text-right totals" colspan="3">
 			{{ d.get_formatted("base_tax_amount") }}
 		</td>
 	</tr>
@@ -23,76 +23,62 @@
 {% endfor %}
 
 {% if doc.doctype == 'Quotation' %}
-{% if doc.coupon_code %}
-<tr>
-	<th class="text-right" colspan="2">
-		{{ _("Discount") }}
-	</th>
-	<th class="text-right tot_quotation_discount">
-		{% set tot_quotation_discount = [] %}
-		{%- for item in doc.items -%}
-		{% if tot_quotation_discount.append((((item.price_list_rate * item.qty)
-			* item.discount_percentage) / 100)) %}{% endif %}
-		{% endfor %}
-		{{ frappe.utils.fmt_money((tot_quotation_discount | sum),currency=doc.currency) }}
-	</th>
-</tr>
-{% endif %}
+	{% if doc.coupon_code %}
+		<tr>
+			<td class="text-left total-discount" colspan="1">
+				{{ _("Savings") }}
+			</td>
+			<td class="text-right tot_quotation_discount total-discount totals" colspan="3">
+				{% set tot_quotation_discount = [] %}
+				{%- for item in doc.items -%}
+					{% if tot_quotation_discount.append((((item.price_list_rate * item.qty)
+						* item.discount_percentage) / 100)) %}
+					{% endif %}
+				{% endfor %}
+				{{ frappe.utils.fmt_money((tot_quotation_discount | sum),currency=doc.currency) }}
+			</td>
+		</tr>
+	{% endif %}
 {% endif %}
 
 {% if doc.doctype == 'Sales Order' %}
-{% if doc.coupon_code %}
-<tr>
-	<th class="text-right" colspan="2">
-		{{ _("Total Amount") }}
-	</th>
-	<th class="text-right">
-		<span>
-		{% set total_amount = [] %}
-		{%- for item in doc.items -%}
-		{% if total_amount.append((item.price_list_rate * item.qty)) %}{% endif %}
-		{% endfor %}
-		{{ frappe.utils.fmt_money((total_amount | sum),currency=doc.currency) }}
-		</span>
-	</th>
-</tr>
-<tr>
-	<th class="text-right" colspan="2">
-		{{ _("Applied Coupon Code") }}
-	</th>
-	<th class="text-right">
-		<span>
-		{%- for row in frappe.get_all(doctype="Coupon Code",
-		fields=["coupon_code"], filters={ "name":doc.coupon_code}) -%}
-			<span>{{ row.coupon_code }}</span>
-		{% endfor %}
-		</span>
-	</th>
-</tr>
-<tr>
-	<th class="text-right" colspan="2">
-		{{ _("Discount") }}
-	</th>
-	<th class="text-right">
-		<span>
-		{% set tot_SO_discount = [] %}
-		{%- for item in doc.items -%}
-		{% if tot_SO_discount.append((((item.price_list_rate * item.qty)
-			* item.discount_percentage) / 100)) %}{% endif %}
-		{% endfor %}
-		{{ frappe.utils.fmt_money((tot_SO_discount | sum),currency=doc.currency) }}
-		</span>
-	</th>
-</tr>
-{% endif %}
+	{% if doc.coupon_code %}
+		<tr>
+			<td class="text-left total-discount" colspan="2" style="padding-right: 2rem;">
+				{{ _("Applied Coupon Code") }}
+			</td>
+			<td class="text-right total-discount">
+				<span>
+				{%- for row in frappe.get_all(doctype="Coupon Code",
+				fields=["coupon_code"], filters={ "name":doc.coupon_code}) -%}
+					<span>{{ row.coupon_code }}</span>
+				{% endfor %}
+				</span>
+			</td>
+		</tr>
+		<tr>
+			<td class="text-left total-discount" colspan="2">
+				{{ _("Savings") }}
+			</td>
+			<td class="text-right total-discount">
+				<span>
+				{% set tot_SO_discount = [] %}
+				{%- for item in doc.items -%}
+				{% if tot_SO_discount.append((((item.price_list_rate * item.qty)
+					* item.discount_percentage) / 100)) %}{% endif %}
+				{% endfor %}
+				{{ frappe.utils.fmt_money((tot_SO_discount | sum),currency=doc.currency) }}
+				</span>
+			</td>
+		</tr>
+	{% endif %}
 {% endif %}
 
 <tr>
-	<th></th>
-	<th class="item-grand-total">
+	<th class="text-left item-grand-total" colspan="1">
 		{{ _("Grand Total") }}
 	</th>
-	<th class="text-right item-grand-total">
+	<th class="text-right item-grand-total totals" colspan="3">
 		{{ doc.get_formatted("grand_total") }}
 	</th>
 </tr>
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..a9369bb 100644
--- a/erpnext/templates/includes/products_as_list.html
+++ b/erpnext/templates/includes/products_as_list.html
@@ -1,5 +1,5 @@
-{% 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 %}
+<!-- Used in Product Search -->
 <a class="product-link product-list-link" href="{{ route|abs_url }}">
 	<div class='row'>
 		<div class='col-xs-3 col-sm-2 product-image-wrapper'>
diff --git a/erpnext/templates/pages/cart.html b/erpnext/templates/pages/cart.html
index c64c634..2b7d9e3 100644
--- a/erpnext/templates/pages/cart.html
+++ b/erpnext/templates/pages/cart.html
@@ -4,13 +4,6 @@
 
 {% block header %}<h3 class="shopping-cart-header mt-2 mb-6">{{ _("Shopping Cart") }}</h1>{% endblock %}
 
-<!--
-{% block script %}
-<script>{% include "templates/includes/cart.js" %}</script>
-{% endblock %}
--->
-
-
 {% block header_actions %}
 {% endblock %}
 
@@ -21,8 +14,9 @@
 {% if doc.items %}
 <div class="cart-container">
 	<div class="row m-0">
-		<div class="col-md-8 frappe-card p-5">
-			<div>
+		<!-- Left section -->
+		<div class="col-md-8">
+			<div class="frappe-card p-5 mb-4">
 				<div id="cart-error" class="alert alert-danger" style="display: none;"></div>
 				<div class="cart-items-header">
 					{{ _('Items') }}
@@ -30,89 +24,82 @@
 				<table class="table mt-3 cart-table">
 					<thead>
 						<tr>
-							<th width="60%">{{ _('Item') }}</th>
+							<th class="item-column">{{ _('Item') }}</th>
 							<th width="20%">{{ _('Quantity') }}</th>
-							{% if cart_settings.enable_checkout %}
-							<th width="20%" class="text-right">{{ _('Subtotal') }}</th>
+							{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}
+								<th width="20" class="text-right column-sm-view">{{ _('Subtotal') }}</th>
 							{% endif %}
+							<th width="10%" class="column-sm-view"></th>
 						</tr>
 					</thead>
 					<tbody class="cart-items">
 						{% include "templates/includes/cart/cart_items.html" %}
 					</tbody>
-					{% if cart_settings.enable_checkout %}
-					<tfoot class="cart-tax-items">
-						{% include "templates/includes/order/order_taxes.html" %}
-					</tfoot>
+
+					{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}
+						<tfoot class="cart-tax-items">
+							{% include "templates/includes/cart/cart_items_total.html" %}
+						</tfoot>
 					{% endif %}
 				</table>
-			</div>
-			<div class="row">
-				<div class="col-4">
-					{% if cart_settings.enable_checkout %}
-					<a class="btn btn-outline-primary" href="/orders">
-						{{ _('See past orders') }}
-					</a>
-					{% else %}
-					<a class="btn btn-outline-primary" href="/quotations">
-						{{ _('See past quotations') }}
-					</a>
-					{% endif %}
-				</div>
-				<div class="col-8">
-					{% if doc.items %}
-					<div class="place-order-container">
-						<a class="btn btn-primary-light mr-2" href="/all-products">
-							{{ _("Continue Shopping") }}
-						</a>
+
+				<div class="row mt-2">
+					<div class="col-3">
 						{% if cart_settings.enable_checkout %}
-							<button class="btn btn-primary btn-place-order" type="button">
-								{{ _("Place Order") }}
-							</button>
+							<a class="btn btn-primary-light font-md" href="/orders">
+								{{ _('Past Orders') }}
+							</a>
 						{% else %}
-							<button class="btn btn-primary btn-request-for-quotation" type="button">
-								{{ _("Request for Quotation") }}
-							</button>
+							<a class="btn btn-primary-light font-md" href="/quotations">
+								{{ _('Past Quotes') }}
+							</a>
 						{% endif %}
 					</div>
-					{% endif %}
+					<div class="col-9">
+						{% if doc.items %}
+						<div class="place-order-container">
+							<a class="btn btn-primary-light mr-2 font-md" href="/all-products">
+								{{ _('Continue Shopping') }}
+							</a>
+						</div>
+						{% endif %}
+					</div>
 				</div>
 			</div>
 
-
+			<!-- Terms and Conditions -->
 			{% if doc.items %}
-			{% if doc.tc_name %}
-				<div class="terms-and-conditions-link">
-					<a href class="link-terms-and-conditions" data-terms-name="{{ doc.tc_name }}">
-						{{ _("Terms and Conditions") }}
-					</a>
-					<script>
-						frappe.ready(() => {
-							$('.link-terms-and-conditions').click((e) => {
-								e.preventDefault();
-								const $link = $(e.target);
-								const terms_name = $link.attr('data-terms-name');
-								show_terms_and_conditions(terms_name);
-							})
-						});
-						function show_terms_and_conditions(terms_name) {
-							frappe.call('erpnext.shopping_cart.cart.get_terms_and_conditions', { terms_name })
-							.then(r => {
-								frappe.msgprint({
-									title: terms_name,
-									message: r.message
-								});
-							});
-						}
-					</script>
-				</div>
-			{% endif %}
+				{% if doc.terms %}
+					<div class="t-and-c-container mt-4 frappe-card">
+						<h5>{{ _("Terms and Conditions") }}</h5>
+						<div class="t-and-c-terms mt-2">
+							{{ doc.terms }}
+						</div>
+					</div>
+				{% endif %}
 		</div>
 
+		<!-- Right section -->
 		<div class="col-md-4">
-			<div class="cart-addresses">
-				{% include "templates/includes/cart/cart_address.html" %}
+			<div class="cart-payment-addresses">
+				<!-- Apply Coupon Code  -->
+				{% set show_coupon_code = cart_settings.show_apply_coupon_code_in_website and cart_settings.enable_checkout %}
+				{% if show_coupon_code == 1%}
+					<div class="mb-3">
+						<div class="row no-gutters">
+							<input type="text" class="txtcoupon form-control mr-3 w-50 font-md" placeholder="Enter Coupon Code" name="txtcouponcode"  ></input>
+							<button class="btn btn-primary btn-sm bt-coupon font-md">{{ _("Apply Coupon Code") }}</button>
+							<input type="hidden" class="txtreferral_sales_partner font-md" placeholder="Enter Sales Partner" name="txtreferral_sales_partner" type="text"></input>
+							</div>
+					</div>
+				{% endif %}
+
+				<div class="mb-3 frappe-card p-5 payment-summary">
+					{% include "templates/includes/cart/cart_payment_summary.html" %}
 				</div>
+
+				{% include "templates/includes/cart/cart_address.html" %}
+			</div>
 		</div>
 		{% endif %}
 	</div>
@@ -124,11 +111,11 @@
 	</div>
 	<div class="cart-empty-message mt-4">{{ _('Your cart is Empty') }}</p>
 	{% if cart_settings.enable_checkout %}
-		<a class="btn btn-outline-primary" href="/orders">
+		<a class="btn btn-outline-primary" href="/orders" style="font-size: 16px;">
 			{{ _('See past orders') }}
 		</a>
 		{% else %}
-		<a class="btn btn-outline-primary" href="/quotations">
+		<a class="btn btn-outline-primary" href="/quotations" style="font-size: 16px;">
 			{{ _('See past quotations') }}
 		</a>
 	{% endif %}
diff --git a/erpnext/templates/pages/cart.py b/erpnext/templates/pages/cart.py
index 0bba1ff..cadb46f 100644
--- a/erpnext/templates/pages/cart.py
+++ b/erpnext/templates/pages/cart.py
@@ -1,11 +1,11 @@
-# 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
 
 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):
+	context.body_class = "product-page"
 	context.update(get_cart_quotation())
diff --git a/erpnext/templates/pages/cart_terms.html b/erpnext/templates/pages/cart_terms.html
deleted file mode 100644
index 6d84fb8..0000000
--- a/erpnext/templates/pages/cart_terms.html
+++ /dev/null
@@ -1,2 +0,0 @@
-
-<div>{{doc.terms}}</div>
diff --git a/erpnext/templates/pages/customer_reviews.html b/erpnext/templates/pages/customer_reviews.html
new file mode 100644
index 0000000..121bec3
--- /dev/null
+++ b/erpnext/templates/pages/customer_reviews.html
@@ -0,0 +1,67 @@
+{% 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 reviews-full-page col-md-12">
+	{% if enable_reviews %}
+		<!-- Title and Action -->
+		<div class="w-100 mb-6 d-flex">
+			<div class="reviews-header col-9">
+				{{ _("Customer Reviews") }}
+			</div>
+
+			<div class="write-a-review-btn col-3">
+				<!-- Write a Review for legitimate users -->
+				{% if frappe.session.user != "Guest" and user_is_customer %}
+					<button class="btn btn-write-review"
+						data-web-item="{{ web_item }}">
+						{{ _("Write a Review") }}
+					</button>
+				{% endif %}
+			</div>
+		</div>
+
+		<!-- Summary -->
+		{{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating, for_summary=True, total_reviews=total_reviews) }}
+
+
+		<!-- Reviews and Comments -->
+		<div class="mt-8">
+			{% 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>
+	{% else %}
+		<!-- If reviews are disabled -->
+		<div class="text-center">
+			<h3 class="text-muted mt-8">
+				{{ _("No Reviews") }}
+			</h3>
+		</div>
+	{% endif %}
+</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.py b/erpnext/templates/pages/customer_reviews.py
new file mode 100644
index 0000000..c1f0c93
--- /dev/null
+++ b/erpnext/templates/pages/customer_reviews.py
@@ -0,0 +1,25 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+import frappe
+
+from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
+	get_shopping_cart_settings,
+)
+from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
+from erpnext.e_commerce.doctype.website_item.website_item import check_if_user_is_customer
+
+
+def get_context(context):
+	context.body_class = "product-page"
+	context.no_cache = 1
+	context.full_page = True
+	context.reviews = None
+
+	if frappe.form_dict and frappe.form_dict.get("web_item"):
+		context.web_item = frappe.form_dict.get("web_item")
+		context.user_is_customer = check_if_user_is_customer()
+		context.enable_reviews = get_shopping_cart_settings().enable_reviews
+
+		if context.enable_reviews:
+			reviews_data = get_item_reviews(context.web_item)
+			context.update(reviews_data)
diff --git a/erpnext/templates/pages/home.py b/erpnext/templates/pages/home.py
index 5d046a8..d08e81b 100644
--- a/erpnext/templates/pages/home.py
+++ b/erpnext/templates/pages/home.py
@@ -10,7 +10,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.html b/erpnext/templates/pages/order.html
index 28faea8..a10870d 100644
--- a/erpnext/templates/pages/order.html
+++ b/erpnext/templates/pages/order.html
@@ -12,172 +12,173 @@
 {% endblock %}
 
 {% block header_actions %}
-<div class="dropdown">
-	<button class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
-		<span>{{ _('Actions') }}</span>
-		<b class="caret"></b>
-	</button>
-	<ul class="dropdown-menu dropdown-menu-right" role="menu">
-		{% if doc.doctype == 'Purchase Order' %}
-		<a class="dropdown-item" href="/api/method/erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_invoice_from_portal?purchase_order_name={{ doc.name }}" data-action="make_purchase_invoice">{{ _("Make Purchase Invoice") }}</a>
-		{% endif %}
-		<a class="dropdown-item" href='/printview?doctype={{ doc.doctype}}&name={{ doc.name }}&format={{ print_format }}'
-			target="_blank" rel="noopener noreferrer">
-			{{ _("Print") }}
-		</a>
-	</ul>
-</div>
-
+	<div class="dropdown">
+		<button class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
+			<span class="font-md">{{ _('Actions') }}</span>
+			<b class="caret"></b>
+		</button>
+		<ul class="dropdown-menu dropdown-menu-right" role="menu">
+			{% if doc.doctype == 'Purchase Order' %}
+				<a class="dropdown-item" href="/api/method/erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_invoice_from_portal?purchase_order_name={{ doc.name }}" data-action="make_purchase_invoice">{{ _("Make Purchase Invoice") }}</a>
+			{% endif %}
+			<a class="dropdown-item" href='/printview?doctype={{ doc.doctype}}&name={{ doc.name }}&format={{ print_format }}'
+				target="_blank" rel="noopener noreferrer">
+				{{ _("Print") }}
+			</a>
+		</ul>
+	</div>
 {% endblock %}
 
 {% block page_content %}
-
-<div class="row transaction-subheading">
-	<div class="col-6">
-		<span class="indicator-pill {{ doc.indicator_color or ("blue" if doc.docstatus==1 else "darkgrey") }}">
-			{% if doc.doctype == "Quotation" and not doc.docstatus %}
-				{{ _("Pending") }}
-			{% else %}
-				{{ _(doc.get('indicator_title')) or _(doc.status) or _("Submitted") }}
-			{% endif %}
-		</span>
-	</div>
-	<div class="col-6 text-muted text-right small pt-3">
-		{{ frappe.utils.format_date(doc.transaction_date, 'medium') }}
-		{% if doc.valid_till %}
-		<p>
-		{{ _("Valid Till") }}: {{ frappe.utils.format_date(doc.valid_till, 'medium') }}
-		</p>
-		{% endif %}
-	</div>
-</div>
-
-<p class="small my-3">
-	{%- set party_name = doc.supplier_name if doc.doctype in ['Supplier Quotation', 'Purchase Invoice', 'Purchase Order'] else doc.customer_name %}
-	<b>{{ party_name }}</b>
-
-	{% if doc.contact_display and doc.contact_display != party_name %}
-		<br>
-		{{ doc.contact_display }}
-	{% endif %}
-</p>
-
-{% if doc._header %}
-{{ doc._header }}
-{% endif %}
-
-<div class="order-container">
-	<!-- items -->
-	<table class="order-item-table w-100 table">
-		<thead class="order-items order-item-header">
-			<th width="60%">
-				{{ _("Item") }}
-			</th>
-			<th width="20%" class="text-right">
-				{{ _("Quantity") }}
-			</th>
-			<th width="20%" class="text-right">
-				{{ _("Amount") }}
-			</th>
-		</thead>
-		<tbody>
-		{% for d in doc.items %}
-		<tr class="order-items">
-			<td>
-				{{ item_name_and_description(d) }}
-			</td>
-			<td class="text-right">
-				{{ d.qty }}
-				{% if d.delivered_qty is defined and d.delivered_qty != None %}
-				<p class="text-muted small">{{ _("Delivered") }}&nbsp;{{ d.delivered_qty }}</p>
+	<div class="row transaction-subheading">
+		<div class="col-6">
+			<span class="font-md indicator-pill {{ doc.indicator_color or ("blue" if doc.docstatus==1 else "darkgrey") }}">
+				{% if doc.doctype == "Quotation" and not doc.docstatus %}
+					{{ _("Pending") }}
+				{% else %}
+					{{ _(doc.get('indicator_title')) or _(doc.status) or _("Submitted") }}
 				{% endif %}
-			</td>
-			<td class="text-right">
-				{{ d.get_formatted("amount")	 }}
-				<p class="text-muted small">{{ _("Rate:") }}&nbsp;{{ d.get_formatted("rate") }}</p>
-			</td>
-		</tr>
-		{% endfor %}
-		</tbody>
-	</table>
-	<!-- taxes -->
-	<div class="order-taxes d-flex justify-content-end">
-		<table>
-			{% include "erpnext/templates/includes/order/order_taxes.html" %}
-		</table>
-	</div>
-</div>
-
-{% if enabled_checkout and ((doc.doctype=="Sales Order" and doc.per_billed <= 0)
-	or (doc.doctype=="Sales Invoice" and doc.outstanding_amount > 0)) %}
-
-<div class="panel panel-default">
-	<div class="panel-heading">
-		<div class="row">
-			<div class="form-column col-sm-6 address-title">
-				<strong>Payment</strong>
-			</div>
+			</span>
 		</div>
-	</div>
-	<div class="panel-collapse">
-		<div class="panel-body text-muted small">
-			<div class="row">
-				<div class="form-column col-sm-6">
-					{% if available_loyalty_points %}
-					<div class="form-group">
-						<div class="h6">Enter Loyalty Points</div>
-						<div class="control-input-wrapper">
-							<div class="control-input">
-								<input class="form-control" type="number" min="0" max="{{ available_loyalty_points }}" id="loyalty-point-to-redeem">
-							</div>
-							<p class="help-box small text-muted d-none d-sm-block"> Available Points: {{ available_loyalty_points }} </p>
-						</div>
-					</div>
-					{% endif %}
-				</div>
-
-				<div class="form-column col-sm-6">
-					<div id="loyalty-points-status" style="text-align: right"></div>
-					<div class="page-header-actions-block" data-html-block="header-actions">
-						<p>
-							<a href="/api/method/erpnext.accounts.doctype.payment_request.payment_request.make_payment_request?dn={{ doc.name }}&dt={{ doc.doctype }}&submit_doc=1&order_type=Shopping Cart"
-								class="btn btn-primary btn-sm" id="pay-for-order">{{ _("Pay") }} {{ doc.get_formatted("grand_total") }} </a>
-						</p>
-					</div>
-				</div>
-
-			</div>
-
-		</div>
-	</div>
-</div>
-{% endif %}
-
-
-{% if attachments %}
-<div class="order-item-table">
-	<div class="row order-items order-item-header text-muted">
-		<div class="col-sm-12 h6 text-uppercase">
-			{{ _("Attachments") }}
-		</div>
-	</div>
-	<div class="row order-items">
-		<div class="col-sm-12">
-			{% for attachment in attachments %}
-			<p class="small">
-				<a href="{{ attachment.file_url }}" target="blank"> {{ attachment.file_name }} </a>
+		<div class="col-6 text-muted text-right small pt-3">
+			{{ frappe.utils.format_date(doc.transaction_date, 'medium') }}
+			{% if doc.valid_till %}
+			<p>
+			{{ _("Valid Till") }}: {{ frappe.utils.format_date(doc.valid_till, 'medium') }}
 			</p>
-			{% endfor %}
+			{% endif %}
 		</div>
 	</div>
-</div>
-{% endif %}
-</div>
-{% if doc.terms %}
-<div class="terms-and-condition text-muted small">
-	<hr><p>{{ doc.terms }}</p>
-</div>
-{% endif %}
+
+	<p class="small my-3">
+		{%- set party_name = doc.supplier_name if doc.doctype in ['Supplier Quotation', 'Purchase Invoice', 'Purchase Order'] else doc.customer_name %}
+		<b>{{ party_name }}</b>
+
+		{% if doc.contact_display and doc.contact_display != party_name %}
+			<br>
+			{{ doc.contact_display }}
+		{% endif %}
+	</p>
+
+	{% if doc._header %}
+		{{ doc._header }}
+	{% endif %}
+
+	<div class="order-container">
+		<!-- items -->
+		<table class="order-item-table w-100 table">
+			<thead class="order-items order-item-header">
+				<th width="60%">
+					{{ _("Item") }}
+				</th>
+				<th width="20%" class="text-right">
+					{{ _("Quantity") }}
+				</th>
+				<th width="20%" class="text-right">
+					{{ _("Amount") }}
+				</th>
+			</thead>
+			<tbody>
+			{% for d in doc.items %}
+			<tr class="order-items">
+				<td>
+					{{ item_name_and_description(d) }}
+				</td>
+				<td class="text-right">
+					{{ d.qty }}
+					{% if d.delivered_qty is defined and d.delivered_qty != None %}
+						<p class="text-muted small">{{ _("Delivered") }}&nbsp;{{ d.delivered_qty }}</p>
+					{% endif %}
+				</td>
+				<td class="text-right">
+					{{ d.get_formatted("amount")	 }}
+					<p class="text-muted small">{{ _("Rate:") }}&nbsp;{{ d.get_formatted("rate") }}</p>
+				</td>
+			</tr>
+			{% endfor %}
+			</tbody>
+		</table>
+		<!-- taxes -->
+		<div class="order-taxes d-flex justify-content-end">
+			<table>
+				{% include "erpnext/templates/includes/order/order_taxes.html" %}
+			</table>
+		</div>
+	</div>
+
+	{% if enabled_checkout and ((doc.doctype=="Sales Order" and doc.per_billed <= 0)
+		or (doc.doctype=="Sales Invoice" and doc.outstanding_amount > 0)) %}
+		<div class="panel panel-default">
+			<div class="panel-heading">
+				<div class="row">
+					<div class="form-column col-sm-6 address-title">
+						<strong>Payment</strong>
+					</div>
+				</div>
+			</div>
+			<div class="panel-collapse">
+				<div class="panel-body text-muted small">
+					<div class="row">
+						<div class="form-column col-sm-6">
+							{% if available_loyalty_points %}
+							<div class="form-group">
+								<div class="h6">Enter Loyalty Points</div>
+								<div class="control-input-wrapper">
+									<div class="control-input">
+										<input class="form-control" type="number" min="0" max="{{ available_loyalty_points }}" id="loyalty-point-to-redeem">
+									</div>
+									<p class="help-box small text-muted d-none d-sm-block"> Available Points: {{ available_loyalty_points }} </p>
+								</div>
+							</div>
+							{% endif %}
+						</div>
+
+						<div class="form-column col-sm-6">
+							<div id="loyalty-points-status" style="text-align: right"></div>
+							<div class="page-header-actions-block" data-html-block="header-actions">
+								<p class="mt-2" style="float: right;">
+									<a href="/api/method/erpnext.accounts.doctype.payment_request.payment_request.make_payment_request?dn={{ doc.name }}&dt={{ doc.doctype }}&submit_doc=1&order_type=Shopping Cart"
+										class="btn btn-primary btn-sm"
+										id="pay-for-order">
+										{{ _("Pay") }} {{ doc.get_formatted("grand_total") }}
+									</a>
+								</p>
+							</div>
+						</div>
+
+					</div>
+
+				</div>
+			</div>
+		</div>
+	{% endif %}
+
+
+	{% if attachments %}
+		<div class="order-item-table">
+			<div class="row order-items order-item-header text-muted">
+				<div class="col-sm-12 h6 text-uppercase">
+					{{ _("Attachments") }}
+				</div>
+			</div>
+			<div class="row order-items">
+				<div class="col-sm-12">
+					{% for attachment in attachments %}
+					<p class="small">
+						<a href="{{ attachment.file_url }}" target="blank"> {{ attachment.file_name }} </a>
+					</p>
+					{% endfor %}
+				</div>
+			</div>
+		</div>
+	{% endif %}
+	</div>
+
+	{% if doc.terms %}
+		<div class="terms-and-condition text-muted small">
+			<hr><p>{{ doc.terms }}</p>
+		</div>
+	{% endif %}
 {% endblock %}
 
 {% block script %}
diff --git a/erpnext/templates/pages/order.py b/erpnext/templates/pages/order.py
index 2aa0f9c..712b141 100644
--- a/erpnext/templates/pages/order.py
+++ b/erpnext/templates/pages/order.py
@@ -1,13 +1,10 @@
 # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
-
 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):
@@ -25,7 +22,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 5aa1f1e..237adf9 100644
--- a/erpnext/templates/pages/product_search.py
+++ b/erpnext/templates/pages/product_search.py
@@ -1,12 +1,19 @@
-# 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
 
-
 import frappe
-from frappe.utils import cint, cstr, nowdate
+from frappe.utils import cint, cstr
+from redisearch import AutoCompleter, Client, Query
 
+from erpnext.e_commerce.redisearch_utils import (
+	WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE,
+	WEBSITE_ITEM_INDEX,
+	WEBSITE_ITEM_NAME_AUTOCOMPLETE,
+	is_search_module_loaded,
+	make_key,
+)
+from erpnext.e_commerce.shopping_cart.product_info import set_product_info_for_website
 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
 
 no_cache = 1
 
@@ -15,36 +22,116 @@
 
 @frappe.whitelist(allow_guest=True)
 def get_product_list(search=None, start=0, limit=12):
-	# limit = 12 because we show 12 items in the grid view
-
-	# 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)"""
-
-	# search term condition
-	if search:
-		query += """ and (I.web_long_description like %(search)s
-				or I.description like %(search)s
-				or I.item_name like %(search)s
-				or I.name like %(search)s)"""
-		search = "%" + cstr(search) + "%"
-
-	# order by
-	query += """ order by I.weightage desc, in_stock desc, I.modified desc limit %s, %s""" % (cint(start), cint(limit))
-
-	data = frappe.db.sql(query, {
-		"search": search,
-		"today": nowdate()
-	}, as_dict=1)
+	data = get_product_data(search, start, limit)
 
 	for item in data:
 		set_product_info_for_website(item)
 
 	return [get_item_for_list_in_html(r) for r in data]
+
+def get_product_data(search=None, start=0, limit=12):
+	# limit = 12 because we show 12 items in the grid view
+	# base query
+	query = """
+		SELECT
+			web_item_name, item_name, item_code, brand, route,
+			website_image, thumbnail, item_group,
+			description, web_long_description as website_description,
+			website_warehouse, ranking
+		FROM `tabWebsite Item`
+		WHERE published = 1
+		"""
+
+	# search term condition
+	if search:
+		query += """ and (item_name like %(search)s
+				or web_item_name like %(search)s
+				or brand like %(search)s
+				or web_long_description like %(search)s)"""
+		search = "%" + cstr(search) + "%"
+
+	# order by
+	query += """ ORDER BY ranking desc, modified desc limit %s, %s""" % (cint(start), cint(limit))
+
+	return frappe.db.sql(query, {"search": search}, as_dict=1) # nosemgrep
+
+@frappe.whitelist(allow_guest=True)
+def search(query):
+	product_results = product_search(query)
+	category_results = get_category_suggestions(query)
+
+	return {
+		"product_results": product_results.get("results") or [],
+		"category_results": category_results.get("results") or []
+	}
+
+@frappe.whitelist(allow_guest=True)
+def product_search(query, limit=10, fuzzy_search=True):
+	search_results = {"from_redisearch": True, "results": []}
+
+	if not is_search_module_loaded():
+		# Redisearch module not loaded
+		search_results["from_redisearch"] = False
+		search_results["results"] = get_product_data(query, 0, limit)
+		return search_results
+
+	if not query:
+		return search_results
+
+	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) > 3 # 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)
+
+	results = client.search(q)
+	search_results['results'] = list(map(convert_to_dict, results.docs))
+	search_results['results'] = sorted(search_results['results'], key=lambda k: frappe.utils.cint(k['ranking']), reverse=True)
+
+	return search_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):
+	search_results = {"results": []}
+
+	if not is_search_module_loaded():
+		# Redisearch module not loaded, query db
+		categories = frappe.db.get_all(
+			"Item Group",
+			filters={
+				"name": ["like", "%{0}%".format(query)],
+				"show_in_website": 1
+			},
+			fields=["name", "route"]
+		)
+		search_results['results'] = categories
+		return search_results
+
+	if not query:
+		return search_results
+
+	ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=frappe.cache())
+	suggestions = ac.get_suggestions(query, num=10)
+
+	search_results['results'] = [s.string for s in suggestions]
+
+	return search_results
\ 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..7a81ded
--- /dev/null
+++ b/erpnext/templates/pages/wishlist.html
@@ -0,0 +1,28 @@
+{% 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 %}
+	<div class="cart-empty frappe-card">
+		<div class="cart-empty-state">
+			<img src="/assets/erpnext/images/ui-states/cart-empty-state.png" alt="Empty Cart">
+		</div>
+		<div class="cart-empty-message mt-4">{{ _('Wishlist is empty!') }}</p>
+	</div>
+{% 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..72ee34e
--- /dev/null
+++ b/erpnext/templates/pages/wishlist.py
@@ -0,0 +1,71 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+import frappe
+
+from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
+	get_shopping_cart_settings,
+)
+from erpnext.e_commerce.shopping_cart.cart import _set_price_list
+from erpnext.utilities.product import get_price
+
+
+def get_context(context):
+	is_guest = frappe.session.user == "Guest"
+
+	settings = get_shopping_cart_settings()
+	items = get_wishlist_items() if not is_guest else []
+	selling_price_list = _set_price_list(settings) if not is_guest else None
+
+	items = set_stock_price_details(items, settings, selling_price_list)
+
+	context.body_class = "product-page"
+	context.items = items
+	context.settings = settings
+	context.no_cache = 1
+
+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 bool(stock_qty)
+
+def get_wishlist_items():
+	if not frappe.db.exists("Wishlist", frappe.session.user):
+		return []
+
+	return frappe.db.get_all(
+		"Wishlist Item",
+		filters={
+			"parent": frappe.session.user
+		},
+		fields=[
+			"web_item_name", "item_code", "item_name",
+			"website_item", "warehouse",
+			"image", "item_group", "route"
+		])
+
+def set_stock_price_details(items, settings, selling_price_list):
+	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_price = price_details.get('formatted_price')
+			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')
+
+	return items
\ No newline at end of file
diff --git a/erpnext/tests/test_point_of_sale.py b/erpnext/tests/test_point_of_sale.py
new file mode 100644
index 0000000..df2dc8b
--- /dev/null
+++ b/erpnext/tests/test_point_of_sale.py
@@ -0,0 +1,53 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+
+from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
+from erpnext.selling.page.point_of_sale.point_of_sale import get_items
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+from erpnext.tests.utils import ERPNextTestCase
+
+
+class TestPointOfSale(ERPNextTestCase):
+	def test_item_search(self):
+		"""
+		Test Stock and Service Item Search.
+		"""
+
+		pos_profile = make_pos_profile()
+		item1 = make_item("Test Search Stock Item", {"is_stock_item": 1})
+		make_stock_entry(
+			item_code="Test Search Stock Item",
+			qty=10,
+			to_warehouse="_Test Warehouse - _TC",
+			rate=500,
+		)
+
+		result = get_items(
+			start=0,
+			page_length=20,
+			price_list=None,
+			item_group=item1.item_group,
+			pos_profile=pos_profile.name,
+			search_term="Test Search Stock Item",
+		)
+		filtered_items = result.get("items")
+
+		self.assertEqual(len(filtered_items), 1)
+		self.assertEqual(filtered_items[0]["item_code"], item1.item_code)
+		self.assertEqual(filtered_items[0]["actual_qty"], 10)
+
+		item2 = make_item("Test Search Service Item", {"is_stock_item": 0})
+		result = get_items(
+			start=0,
+			page_length=20,
+			price_list=None,
+			item_group=item2.item_group,
+			pos_profile=pos_profile.name,
+			search_term="Test Search Service Item",
+		)
+		filtered_items = result.get("items")
+
+		self.assertEqual(len(filtered_items), 1)
+		self.assertEqual(filtered_items[0]["item_code"], item2.item_code)
diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py
index e9e4baa..0a45002 100644
--- a/erpnext/utilities/product.py
+++ b/erpnext/utilities/product.py
@@ -1,7 +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
 
-
 import frappe
 from frappe.utils import cint, flt, fmt_money, getdate, nowdate
 
@@ -9,15 +8,15 @@
 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("""
@@ -69,6 +68,8 @@
 	return qty
 
 def get_price(item_code, price_list, customer_group, company, qty=1):
+	from erpnext.e_commerce.shopping_cart.cart import get_party
+
 	template_item_code = frappe.db.get_value("Item", item_code, "variant_of")
 
 	if price_list:
@@ -80,7 +81,8 @@
 				filters={"price_list": price_list, "item_code": template_item_code})
 
 		if price:
-			pricing_rule = get_pricing_rule_for_item(frappe._dict({
+			party = get_party()
+			pricing_rule_dict = frappe._dict({
 				"item_code": item_code,
 				"qty": qty,
 				"stock_qty": qty,
@@ -91,18 +93,33 @@
 				"conversion_rate": 1,
 				"for_shopping_cart": True,
 				"currency": frappe.db.get_value("Price List", price_list, "currency")
-			}))
+			})
+
+			if party and party.doctype == "Customer":
+				pricing_rule_dict.update({"customer": party.name})
+
+			pricing_rule = get_pricing_rule_for_item(pricing_rule_dict)
+			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) \
@@ -123,15 +140,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)
+		bundle_warehouse = frappe.db.get_value("Website Item", {"item_code": item_code}, item_warehouse_field)
+		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/index.html b/erpnext/www/all-products/index.html
index a7838ee..3d5517c 100644
--- a/erpnext/www/all-products/index.html
+++ b/erpnext/www/all-products/index.html
@@ -1,120 +1,34 @@
+{% 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 title %}{{ _('All Products') }}{% endblock %}
 {% block header %}
-<div class="mb-6">{{ _('Products') }}</div>
+<div class="mb-6">{{ _('All Products') }}</div>
 {% endblock header %}
 
 {% block page_content %}
-<div class="row" style="display: none;">
-	<div class="col-8">
-		<div class="input-group input-group-sm mb-3">
-			<input type="search" class="form-control" placeholder="{{_('Search')}}"
-				aria-label="{{_('Product Search')}}" aria-describedby="product-search"
-				value="{{ frappe.sanitize_html(frappe.form_dict.search) or '' }}"
-			>
-		</div>
-	</div>
-
-	<div class="col-4 pl-0">
-		<button class="btn btn-light btn-sm btn-block d-md-none"
-			type="button"
-			data-toggle="collapse"
-			data-target="#product-filters"
-			aria-expanded="false"
-			aria-controls="product-filters"
-			style="white-space: nowrap;"
-		>
-			{{ _('Toggle Filters') }}
-		</button>
-	</div>
-</div>
-
 <div class="row">
-	<div class="col-12 order-2 col-md-9 order-md-2 item-card-group-section">
-		<div class="row products-list">
-			{% if items %}
-				{% for item in items %}
-					{% include "erpnext/www/all-products/item_row.html" %}
-				{% endfor %}
-			{% else %}
-				{% include "erpnext/www/all-products/not_found.html" %}
-			{% endif %}
-		</div>
+	<!-- Items section -->
+	<div id="product-listing" class="col-12 order-2 col-md-9 order-md-2 item-card-group-section">
+		<!-- Rendered via JS -->
 	</div>
+
+	<!-- Filters Section -->
 	<div class="col-12 order-1 col-md-3 order-md-1">
-
-		{% if frappe.form_dict.start or frappe.form_dict.field_filters or frappe.form_dict.attribute_filters or frappe.form_dict.search %}
-
-
-		{% endif  %}
-
 		<div class="collapse d-md-block mr-4 filters-section" id="product-filters">
 			<div class="d-flex justify-content-between align-items-center mb-5 title-section">
 				<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 %}
-
-					{% 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 %}
+			<!-- attribute filters -->
+			{% if attribute_filters %}
+				{{ attribute_filter_section(attribute_filters) }}
+			{% endif %}
 		</div>
 
 		<script>
@@ -137,18 +51,6 @@
 		</script>
 	</div>
 </div>
-<div class="row product-paging-area mt-5">
-	<div class="col-3">
-	</div>
-	<div class="col-9 text-right">
-		{% if frappe.form_dict.start|int > 0 %}
-		<button class="btn btn-default btn-prev" data-start="{{ frappe.form_dict.start|int - page_length }}">{{ _("Prev") }}</button>
-		{% endif %}
-		{% if items|length >= page_length %}
-		<button class="btn btn-default btn-next" data-start="{{ frappe.form_dict.start|int + page_length }}">{{ _("Next") }}</button>
-		{% endif %}
-	</div>
-</div>
 
 <script>
 	frappe.ready(() => {
diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js
index 1c641b5..98a8441 100644
--- a/erpnext/www/all-products/index.js
+++ b/erpnext/www/all-products/index.js
@@ -1,165 +1,27 @@
 $(() => {
 	class ProductListing {
 		constructor() {
-			this.bind_filters();
-			this.bind_search();
-			this.restore_filters_state();
-		}
+			let me = this;
+			let is_item_group_page = $(".item-group-content").data("item-group");
+			this.item_group = is_item_group_page || null;
 
-		bind_filters() {
-			this.field_filters = {};
-			this.attribute_filters = {};
+			let view_type = localStorage.getItem("product_view") || "List View";
 
-			$('.product-filter').on('change', frappe.utils.debounce((e) => {
-				const $checkbox = $(e.target);
-				const is_checked = $checkbox.is(':checked');
-
-				if ($checkbox.is('.attribute-filter')) {
-					const {
-						attributeName: attribute_name,
-						attributeValue: attribute_value
-					} = $checkbox.data();
-
-					if (is_checked) {
-						this.attribute_filters[attribute_name] = this.attribute_filters[attribute_name] || [];
-						this.attribute_filters[attribute_name].push(attribute_value);
-					} else {
-						this.attribute_filters[attribute_name] = this.attribute_filters[attribute_name] || [];
-						this.attribute_filters[attribute_name] = this.attribute_filters[attribute_name].filter(v => v !== attribute_value);
-					}
-
-					if (this.attribute_filters[attribute_name].length === 0) {
-						delete this.attribute_filters[attribute_name];
-					}
-				} else if ($checkbox.is('.field-filter')) {
-					const {
-						filterName: filter_name,
-						filterValue: filter_value
-					} = $checkbox.data();
-
-					if (is_checked) {
-						this.field_filters[filter_name] = this.field_filters[filter_name] || [];
-						this.field_filters[filter_name].push(filter_value);
-					} else {
-						this.field_filters[filter_name] = this.field_filters[filter_name] || [];
-						this.field_filters[filter_name] = this.field_filters[filter_name].filter(v => v !== filter_value);
-					}
-
-					if (this.field_filters[filter_name].length === 0) {
-						delete this.field_filters[filter_name];
-					}
-				}
-
-				const query_string = get_query_string({
-					field_filters: JSON.stringify(if_key_exists(this.field_filters)),
-					attribute_filters: JSON.stringify(if_key_exists(this.attribute_filters)),
-				});
-				window.history.pushState('filters', '', `${location.pathname}?` + query_string);
-
-				$('.page_content input').prop('disabled', true);
-				this.get_items_with_filters()
-					.then(html => {
-						$('.products-list').html(html);
-					})
-					.then(data => {
-						$('.page_content input').prop('disabled', false);
-						return data;
-					})
-					.catch(() => {
-						$('.page_content input').prop('disabled', false);
-					});
-			}, 1000));
-		}
-
-		make_filters() {
-
-		}
-
-		bind_search() {
-			$('input[type=search]').on('keydown', (e) => {
-				if (e.keyCode === 13) {
-					// Enter
-					const value = e.target.value;
-					if (value) {
-						window.location.search = 'search=' + e.target.value;
-					} else {
-						window.location.search = '';
-					}
-				}
+			// Render Product Views, Filters & Search
+			new erpnext.ProductView({
+				view_type: view_type,
+				products_section: $('#product-listing'),
+				item_group: me.item_group
 			});
+
+			this.bind_card_actions();
 		}
 
-		restore_filters_state() {
-			const filters = frappe.utils.get_query_params();
-			let {field_filters, attribute_filters} = filters;
-
-			if (field_filters) {
-				field_filters = JSON.parse(field_filters);
-				for (let fieldname in field_filters) {
-					const values = field_filters[fieldname];
-					const selector = values.map(value => {
-						return `input[data-filter-name="${fieldname}"][data-filter-value="${value}"]`;
-					}).join(',');
-					$(selector).prop('checked', true);
-				}
-				this.field_filters = field_filters;
-			}
-			if (attribute_filters) {
-				attribute_filters = JSON.parse(attribute_filters);
-				for (let attribute in attribute_filters) {
-					const values = attribute_filters[attribute];
-					const selector = values.map(value => {
-						return `input[data-attribute-name="${attribute}"][data-attribute-value="${value}"]`;
-					}).join(',');
-					$(selector).prop('checked', true);
-				}
-				this.attribute_filters = attribute_filters;
-			}
-		}
-
-		get_items_with_filters() {
-			const { attribute_filters, field_filters } = this;
-			const args = {
-				field_filters: if_key_exists(field_filters),
-				attribute_filters: if_key_exists(attribute_filters)
-			};
-
-			const item_group = $(".item-group-content").data('item-group');
-			if (item_group) {
-				Object.assign(field_filters, { item_group });
-			}
-			return new Promise((resolve, reject) => {
-				frappe.call('erpnext.portal.product_configurator.utils.get_products_html_for_website', args)
-					.then(r => {
-						if (r.exc) reject(r.exc);
-						else resolve(r.message);
-					})
-					.fail(reject);
-			});
+		bind_card_actions() {
+			erpnext.e_commerce.shopping_cart.bind_add_to_cart_action();
+			erpnext.e_commerce.wishlist.bind_wishlist_action();
 		}
 	}
 
 	new ProductListing();
-
-	function get_query_string(object) {
-		const url = new URLSearchParams();
-		for (let key in object) {
-			const value = object[key];
-			if (value) {
-				url.append(key, value);
-			}
-		}
-		return url.toString();
-	}
-
-	function if_key_exists(obj) {
-		let exists = false;
-		for (let key in obj) {
-			if (obj.hasOwnProperty(key) && obj[key]) {
-				exists = true;
-				break;
-			}
-		}
-		return exists ? obj : undefined;
-	}
 });
diff --git a/erpnext/www/all-products/index.py b/erpnext/www/all-products/index.py
index df5258b..ffaead6 100644
--- a/erpnext/www/all-products/index.py
+++ b/erpnext/www/all-products/index.py
@@ -1,36 +1,19 @@
 import frappe
+from frappe.utils import cint
 
-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 erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder
 
 sitemap = 1
 
 def get_context(context):
-
-	if frappe.form_dict:
-		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)
-	else:
-		search = field_filters = attribute_filters = None
-		start = 0
-
-	engine = ProductQuery()
-	context.items = engine.query(attribute_filters, field_filters, search, start)
-
 	# Add homepage as parent
+	context.body_class = "product-page"
 	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()
 
-	context.product_settings = product_settings
-	context.body_class = "product-page"
-	context.page_length = product_settings.products_per_page or 20
+	context.page_length = cint(frappe.db.get_single_value('E Commerce Settings', 'products_per_page'))or 20
 
-	context.no_cache = 1
+	context.no_cache = 1
\ No newline at end of file
diff --git a/erpnext/www/all-products/item_row.html b/erpnext/www/all-products/item_row.html
deleted file mode 100644
index a7e994c..0000000
--- a/erpnext/www/all-products/item_row.html
+++ /dev/null
@@ -1,6 +0,0 @@
-{% 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
-) }}
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..04d2d57
--- /dev/null
+++ b/erpnext/www/shop-by-category/index.html
@@ -0,0 +1,48 @@
+{% 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;
+	}
+</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..3946212
--- /dev/null
+++ b/erpnext/www/shop-by-category/index.py
@@ -0,0 +1,77 @@
+import frappe
+from frappe import _
+
+sitemap = 1
+
+def get_context(context):
+	context.body_class = "product-page"
+
+	settings = frappe.get_cached_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_cached_doc("Website Slideshow", slideshow)
+	slides = slideshow.get({"doctype": "Website Slideshow Item"})
+	for index, slide in enumerate(slides, start=1):
+		values[f"slide_{index}_image"] = slide.image
+		values[f"slide_{index}_title"] = slide.heading
+		values[f"slide_{index}_subtitle"] = slide.description
+		values[f"slide_{index}_theme"] = slide.get("theme") or "Light"
+		values[f"slide_{index}_content_align"] = slide.get("content_align") or "Centre"
+		values[f"slide_{index}_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, start=1):
+		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.get_all(
+				"Item Group",
+				filters={
+					"parent_item_group": "All Item Groups",
+					"show_in_website": 1
+				},
+				fields=["name", "parent_item_group", "is_group", "image", "route"],
+				as_dict=True
+			)
+		else:
+			doctype = frappe.unscrub(category)
+			fields = ["name"]
+			if frappe.get_meta(doctype, cached=True).get_field("image"):
+				fields += ["image"]
+
+			categorical_data[category] = frappe.db.get_all(doctype, fields=fields, as_dict=True)
+
+	return categorical_data
+
diff --git a/requirements.txt b/requirements.txt
index f447fac..39591ca 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,3 +10,4 @@
 taxjar~=1.9.2
 tweepy~=3.10.0
 Unidecode~=1.2.0
+redisearch==2.0.0
\ No newline at end of file