Merge pull request #24603 from marination/e-commerce-refactor

This commit is contained in:
Suraj Shetty
2021-09-03 14:07:52 +05:30
committed by GitHub
167 changed files with 9133 additions and 4239 deletions

View File

@@ -41,9 +41,6 @@ def test_create_test_data():
"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

View File

@@ -286,7 +286,7 @@ class PaymentRequest(Document):
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
@@ -436,7 +436,7 @@ def get_gateway_details(args):
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})

View File

@@ -100,7 +100,7 @@ class TaxRule(Document):
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

View File

@@ -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"
}

View File

@@ -131,7 +131,7 @@ def find_variant(template, args, variant_item_code=None):
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:
@@ -261,9 +261,8 @@ def generate_keyed_value_combinations(args):
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", "has_variants", "attributes"]
if item.variant_based_on=='Manufacturer':
# don't copy manufacturer values if based on part no

View File

@@ -54,7 +54,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -138,7 +137,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -220,7 +218,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -302,7 +299,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -384,7 +380,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -466,7 +461,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -548,7 +542,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -630,7 +623,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -712,7 +704,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -794,7 +785,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -876,7 +866,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -958,7 +947,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1040,7 +1028,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1122,7 +1109,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1204,7 +1190,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1286,7 +1271,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1368,7 +1352,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1450,7 +1433,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1532,7 +1514,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1614,7 +1595,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1696,7 +1676,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1778,7 +1757,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1860,7 +1838,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -1942,7 +1919,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2024,7 +2000,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2106,7 +2081,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2188,7 +2162,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2270,7 +2243,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2352,7 +2324,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2434,7 +2405,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2516,7 +2486,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2598,7 +2567,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2680,7 +2648,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2762,7 +2729,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2844,7 +2810,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -2926,7 +2891,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3008,7 +2972,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3092,7 +3055,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3174,7 +3136,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3256,7 +3217,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3338,7 +3298,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3420,7 +3379,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3502,7 +3460,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3584,7 +3541,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3666,7 +3622,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3748,7 +3703,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3830,7 +3784,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3912,7 +3865,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -3994,7 +3946,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4076,7 +4027,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4158,7 +4108,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4240,7 +4189,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4322,7 +4270,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4404,7 +4351,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4486,7 +4432,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4568,7 +4513,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4650,7 +4594,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4732,7 +4675,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4814,7 +4756,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4896,7 +4837,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -4978,7 +4918,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -5060,7 +4999,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,
@@ -5142,7 +5080,6 @@
"safety_stock": 0.0,
"selling_cost_center": null,
"serial_no_series": null,
"show_in_website": 0,
"show_variant_in_website": 0,
"slideshow": null,
"standard_rate": 0.0,

83
erpnext/e_commerce/api.py Normal file
View File

@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
import json
from frappe.utils import cint
from erpnext.e_commerce.product_data_engine.query import ProductQuery
from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder
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)
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")

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Shopping Cart Settings", {
frappe.ui.form.on("E Commerce Settings", {
onload: function(frm) {
if(frm.doc.__onload && frm.doc.__onload.quotation_series) {
frm.fields_dict.quotation_series.df.options = frm.doc.__onload.quotation_series;
@@ -23,6 +23,21 @@ frappe.ui.form.on("Shopping Cart Settings", {
</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) {

View File

@@ -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
}

View File

@@ -1,25 +1,77 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _, msgprint
from frappe.utils import flt
from frappe.utils import flt, comma_and
from frappe.model.document import Document
from frappe.utils import get_datetime, get_datetime_str, now_datetime
from frappe.utils import unique
from erpnext.e_commerce.redisearch import create_website_items_index, get_indexable_web_fields, is_search_module_loaded
class ShoppingCartSetupError(frappe.ValidationError): pass
class ShoppingCartSettings(Document):
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
@@ -60,12 +112,23 @@ class ShoppingCartSettings(Document):
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 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, method):
frappe.get_doc("E Commerce Settings", "E Commerce Settings").run_method("validate")
def get_shopping_cart_settings():
if not getattr(frappe.local, "shopping_cart_settings", None):
frappe.local.shopping_cart_settings = frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings")
frappe.local.shopping_cart_settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
return frappe.local.shopping_cart_settings

View File

@@ -1,19 +1,18 @@
# 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
from __future__ import unicode_literals
import frappe
import unittest
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import ShoppingCartSetupError
class TestShoppingCartSettings(unittest.TestCase):
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ShoppingCartSetupError
class TestECommerceSettings(unittest.TestCase):
def setUp(self):
frappe.db.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """)
def get_cart_settings(self):
return frappe.get_doc({"doctype": "Shopping Cart Settings",
return frappe.get_doc({"doctype": "E Commerce Settings",
"company": "_Test Company"})
# NOTE: Exchangrate API has all enabled currencies that ERPNext supports.
@@ -47,4 +46,13 @@ class TestShoppingCartSettings(unittest.TestCase):
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"]

View File

@@ -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) {
// }
});

View File

@@ -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
}

View File

@@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
from datetime import datetime
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.contacts.doctype.contact.contact import get_contact_name
from frappe.utils import flt, cint
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import get_shopping_cart_settings
class 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)

View File

@@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
import unittest
from frappe.core.doctype.user_permission.test_user_permission import create_user
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews, \
add_item_review, UnverifiedReviewer
from erpnext.e_commerce.shopping_cart.cart import get_party
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import setup_e_commerce_settings
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")

View File

@@ -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
}

View File

@@ -0,0 +1,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 RecommendedItems(Document):
pass

View File

@@ -0,0 +1,7 @@
{% extends "templates/web.html" %}
{% block page_content %}
<h1>{{ title }}</h1>
{% endblock %}
<!-- this is a sample default web page template -->

View File

@@ -0,0 +1,4 @@
<div>
<a href="{{ doc.route }}">{{ doc.title or doc.name }}</a>
</div>
<!-- this is a sample default list template -->

View File

@@ -0,0 +1,490 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from erpnext.stock.doctype.item.item import DataValidationError
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
from erpnext.controllers.item_variant import create_variant
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import setup_e_commerce_settings
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import get_shopping_cart_settings
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
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_regular_web_item()
make_web_item_price(item_code="Test Mobile Phone")
make_web_pricing_rule(
title="Test Pricing Rule for Test Mobile Phone",
item_code="Test Mobile Phone",
selling=1)
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
# 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"), 10)
self.assertEqual(price_object.get("price_list_rate"), 900)
self.assertEqual(price_object.get("formatted_mrp"), "₹ 1,000.00")
self.assertEqual(price_object.get("formatted_price"), "₹ 900.00")
self.assertEqual(price_object.get("formatted_discount_percent"), "10%")
# disable show price
setup_e_commerce_settings({"show_price": 0})
# 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_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"
})
pricing_rule.insert()
else:
pricing_rule = frappe.get_doc("Pricing Rule", {"title": title})
return pricing_rule

View File

@@ -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);
}
});

View File

@@ -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
}

View File

@@ -0,0 +1,524 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
import json
import itertools
from frappe import _
from frappe.website.website_generator import WebsiteGenerator
from frappe.utils import cstr, random_string, cint, flt
from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow
from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, invalidate_cache_for)
from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
from erpnext.e_commerce.shopping_cart.cart import _set_price_list
from erpnext.utilities.product import get_price
# SEARCH
from erpnext.e_commerce.redisearch import (
insert_item_to_index,
update_index_for_item,
delete_item_from_index
)
class WebsiteItem(WebsiteGenerator):
website = frappe._dict(
page_title_field="web_item_name",
condition_field="published",
template="templates/generators/item/item.html",
no_cache=1
)
def onload(self):
super(WebsiteItem, self).onload()
def validate(self):
super(WebsiteItem, self).validate()
if not self.item_code:
frappe.throw(_("Item Code is required"), title=_("Mandatory"))
self.validate_duplicate_website_item()
self.validate_website_image()
self.make_thumbnail()
self.publish_unpublish_desk_item(publish=True)
if not self.get("__islocal"):
self.old_website_item_groups = frappe.db.sql_list("""
select
item_group
from
`tabWebsite Item Group`
where
parentfield='website_item_groups'
and parenttype='Website Item'
and parent=%s
""", self.name)
def on_update(self):
invalidate_cache_for_web_item(self)
self.update_template_item()
def on_trash(self):
super(WebsiteItem, self).on_trash()
delete_item_from_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):
if frappe.flags.in_import:
return
"""Make a thumbnail of `website_image`"""
import requests.exceptions
if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"):
self.thumbnail = None
if self.website_image and not self.thumbnail:
file_doc = None
try:
file_doc = frappe.get_doc("File", {
"file_url": self.website_image,
"attached_to_doctype": "Website Item",
"attached_to_name": self.name
})
except frappe.DoesNotExistError:
pass
# cleanup
frappe.local.message_log.pop()
except requests.exceptions.HTTPError:
frappe.msgprint(_("Warning: Invalid attachment {0}").format(self.website_image))
self.website_image = None
except requests.exceptions.SSLError:
frappe.msgprint(
_("Warning: Invalid SSL certificate on attachment {0}").format(self.website_image))
self.website_image = None
# for CSV import
if self.website_image and not file_doc:
try:
file_doc = frappe.get_doc({
"doctype": "File",
"file_url": self.website_image,
"attached_to_doctype": "Website Item",
"attached_to_name": self.name
}).save()
except IOError:
self.website_image = None
if file_doc:
if not file_doc.thumbnail_url:
file_doc.make_thumbnail()
self.thumbnail = file_doc.thumbnail_url
def get_context(self, context):
context.show_search = True
context.search_link = "/search"
context.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_variant_context(context)
self.set_attribute_context(context)
self.set_disabled_attributes(context)
self.set_metatags(context)
self.set_shopping_cart_data(context)
settings = context.shopping_cart.cart_settings
self.get_product_details_section(context)
if settings.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_variant_context(self, context):
if not self.has_variants:
return
context.no_cache = True
variant = frappe.form_dict.variant
# load variants
# also used in set_attribute_context
context.variants = frappe.get_all(
"Item",
filters={
"variant_of": self.item_code,
"published_in_website": 1
},
order_by="name asc")
# the case when the item is opened for the first time from its list
if not variant and context.variants:
variant = context.variants[0]
if variant:
context.variant = frappe.get_doc("Item", variant)
fields = ("website_image", "website_image_alt", "web_long_description", "description",
"website_specifications")
for fieldname in fields:
if context.variant.get(fieldname):
value = context.variant.get(fieldname)
if isinstance(value, list):
value = [d.as_dict() for d in value]
context[fieldname] = value
if self.slideshow and context.variant and context.variant.slideshow:
context.update(get_slideshow(context.variant))
def set_attribute_context(self, context):
if not self.has_variants:
return
attribute_values_available = {}
context.attribute_values = {}
context.selected_attributes = {}
# load attributes
self.set_selected_attributes(context.variants, context, attribute_values_available)
# filter attributes, order based on attribute table
item = frappe.get_cached_doc("Item", self.item_code)
self.set_attribute_values(item.attributes, context, attribute_values_available)
context.variant_info = json.dumps(context.variants)
def set_selected_attributes(self, variants, context, attribute_values_available):
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_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.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)
@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 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):
items = frappe.db.sql(f"""
select
ri.website_item_thumbnail, ri.website_item_name,
ri.route, ri.item_code
from
`tabRecommended Items` ri, `tabWebsite Item` wi
where
ri.item_code = wi.item_code
and ri.parent = '{self.name}'
and wi.published = 1
order by ri.idx
""", as_dict=1)
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]

View File

@@ -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"];
}
}
};

View File

@@ -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
}

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class WebsiteItemTabbedSection(Document):
pass

View File

@@ -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
}

View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class WebsiteOffer(Document):
pass
@frappe.whitelist(allow_guest=True)
def get_offer_details(offer_id):
return frappe.db.get_value('Website Offer', {'name': offer_id}, ['offer_details'])

View File

@@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
import unittest
from frappe.core.doctype.user_permission.test_user_permission import create_user
from erpnext.stock.doctype.item.test_item import make_item
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
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()

View File

@@ -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) {
// }
});

View File

@@ -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
}

View File

@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class Wishlist(Document):
pass
@frappe.whitelist()
def add_to_wishlist(item_code):
"""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()
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)))

View File

@@ -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
}

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class WishlistItem(Document):
pass

View File

@@ -6,6 +6,7 @@ from whoosh.qparser import MultifieldParser, FieldsPlugin, WildcardPlugin
from whoosh.analysis import StemmingAnalyzer
from whoosh.query import Prefix
# TODO: Make obsolete
INDEX_NAME = "products"
class ProductSearch(FullTextSearch):
@@ -111,7 +112,7 @@ class ProductSearch(FullTextSearch):
)
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)

View File

@@ -0,0 +1,149 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe import _dict
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]
attribute_docs = [
frappe.get_doc('Item Attribute', attribute) for attribute in attributes
]
valid_attributes = []
for attr_doc in attribute_docs:
selected_attributes = []
for attr in attr_doc.item_attribute_values:
or_filters = []
filters= [
["Item Variant Attribute", "attribute", "=", attr.parent],
["Item Variant Attribute", "attribute_value", "=", attr.attribute_value]
]
if self.item_group:
or_filters.extend([
["item_group", "=", self.item_group],
["Website Item Group", "item_group", "=", self.item_group]
])
if frappe.db.get_all("Item", filters, or_filters=or_filters, limit=1):
selected_attributes.append(attr)
if selected_attributes:
valid_attributes.append(
_dict(
item_attribute_values=selected_attributes,
name=attr_doc.name
)
)
return valid_attributes
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

View File

@@ -0,0 +1,297 @@
# 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.shopping_cart.product_info import get_product_info_for_website
from erpnext.e_commerce.doctype.item_review.item_review import get_customer
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]]
)
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

View File

@@ -0,0 +1,116 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
import unittest
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)

View File

@@ -0,0 +1,344 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
import unittest
from erpnext.e_commerce.product_data_engine.query import ProductQuery
from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder
from erpnext.e_commerce.doctype.website_item.test_website_item import create_regular_web_item
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import setup_e_commerce_settings
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]
attributes = attribute_filter.item_attribute_values
attribute_values = [d.attribute_value for d in attributes]
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 in the right order
self.assertEqual(len(items), 2)
self.assertEqual(items[0].get("item_code"), "Test 13I Laptop")
self.assertEqual(items[1].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.stock.doctype.item.test_item import make_item
from erpnext.controllers.item_variant import create_variant
from erpnext.e_commerce.doctype.website_item.website_item import make_website_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)

View File

@@ -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 ``;
}
}
};

View File

@@ -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 ``;
}
}
};

View File

@@ -0,0 +1,241 @@
erpnext.ProductSearch = class {
constructor() {
this.MAX_RECENT_SEARCHES = 4;
this.searchBox = $("#search-box");
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('#search-box').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);
}
};

View File

@@ -0,0 +1,538 @@
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();
frappe.require('/assets/js/e-commerce.min.js', function() {
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");
frappe.require('/assets/js/e-commerce.min.js', function() {
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");
frappe.require('/assets/js/e-commerce.min.js', function() {
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;
}
};

View File

@@ -0,0 +1,208 @@
# 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 (Client, AutoCompleter, Suggestion, IndexDefinition, TextField, TagField)
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():
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
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()

View File

@@ -7,10 +7,10 @@ from frappe import throw, _
import frappe.defaults
from frappe.utils import cint, flt, get_fullname, cstr
from frappe.contacts.doctype.address.address import get_address_display
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import get_shopping_cart_settings
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import get_shopping_cart_settings
from frappe.utils.nestedset import get_root_of
from erpnext.accounts.utils import get_account_name
from erpnext.utilities.product import get_qty_in_stock
from erpnext.utilities.product import get_web_item_qty_in_stock
from frappe.contacts.doctype.contact.contact import get_contact_name
@@ -18,7 +18,7 @@ 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")))
@@ -45,7 +45,7 @@ def get_cart_quotation(doc=None):
"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()
@@ -69,7 +69,7 @@ def get_billing_addresses(party=None):
@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
@@ -89,11 +89,17 @@ def place_order():
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))
if item.qty > item_stock.stock_qty[0][0]:
@@ -153,9 +159,8 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False):
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),
@@ -164,8 +169,7 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False):
}
else:
return {
'name': quotation.name,
'shopping_cart_menu': get_shopping_cart_menu(context)
'name': quotation.name
}
@frappe.whitelist()
@@ -259,13 +263,17 @@ def guess_territory():
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))
d.update(frappe.db.get_value(
"Website Item",
{"item_code": d.item_code},
["web_item_name", "thumbnail", "website_image", "description", "route"],
as_dict=True)
)
return doc
@@ -282,7 +290,7 @@ def _get_cart_quotation(party=None):
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-",
@@ -337,7 +345,7 @@ def apply_cart_settings(party=None, quotation=None):
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)
@@ -414,7 +422,7 @@ def get_party(user=None):
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 = ''

View File

@@ -4,10 +4,10 @@
from __future__ import unicode_literals
import frappe
from erpnext.shopping_cart.cart import _get_cart_quotation, _set_price_list
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings \
from erpnext.e_commerce.shopping_cart.cart import _get_cart_quotation, _set_price_list
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings \
import get_shopping_cart_settings, show_quantity_in_website
from erpnext.utilities.product import get_price, get_qty_in_stock, get_non_stock_item_status
from erpnext.utilities.product import get_price, get_web_item_qty_in_stock, get_non_stock_item_status
@frappe.whitelist(allow_guest=True)
def get_product_info_for_website(item_code, skip_quotation_creation=False):
@@ -23,6 +23,12 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False):
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,
@@ -30,18 +36,30 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False):
cart_settings.company
)
stock_status = get_qty_in_stock(item_code, "website_warehouse")
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,
"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 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

View File

@@ -5,10 +5,10 @@ from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import nowdate, add_months
from erpnext.shopping_cart.cart import _get_cart_quotation, update_cart, get_party
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, update_cart, get_party
from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule
# test_dependencies = ['Payment Terms Template']
class TestShoppingCart(unittest.TestCase):
@@ -25,6 +25,11 @@ class TestShoppingCart(unittest.TestCase):
frappe.set_user("Administrator")
create_test_contact_and_address()
self.enable_shopping_cart()
if not frappe.db.exists("Website Item", {"item_code": "_Test Item"}):
make_website_item(frappe.get_cached_doc("Item", "_Test Item"))
if not frappe.db.exists("Website Item", {"item_code": "_Test Item 2"}):
make_website_item(frappe.get_cached_doc("Item", "_Test Item 2"))
def tearDown(self):
frappe.set_user("Administrator")
@@ -164,7 +169,7 @@ class TestShoppingCart(unittest.TestCase):
# 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,
@@ -194,7 +199,7 @@ class TestShoppingCart(unittest.TestCase):
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

View File

@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
import frappe.defaults
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import is_cart_enabled
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import is_cart_enabled
def show_cart_count():
if (is_cart_enabled() and
@@ -18,7 +18,7 @@ def set_cart_count(login_manager):
role, parties = check_customer_or_supplier()
if role == 'Supplier': return
if show_cart_count():
from erpnext.shopping_cart.cart import set_cart_count
from erpnext.e_commerce.shopping_cart.cart import set_cart_count
set_cart_count()
def clear_cart_count(login_manager):

View File

@@ -0,0 +1,10 @@
# import frappe
import unittest
# from erpnext.e_commerce.product_data_engine.query import ProductQuery
# from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
test_dependencies = ["Item"]
class TestVariantSelector(unittest.TestCase):
# TODO: Variant Selector Tests
pass

View File

@@ -0,0 +1,195 @@
import frappe
from frappe.utils import cint
from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager
def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
items = []
for attribute, values in attribute_filters.items():
attribute_values = values
if not isinstance(attribute_values, list):
attribute_values = [attribute_values]
if not attribute_values:
continue
wheres = []
query_values = []
for attribute_value in attribute_values:
wheres.append('( attribute = %s and attribute_value = %s )')
query_values += [attribute, attribute_value]
attribute_query = ' or '.join(wheres)
if template_item_code:
variant_of_query = 'AND t2.variant_of = %s'
query_values.append(template_item_code)
else:
variant_of_query = ''
query = '''
SELECT
t1.parent
FROM
`tabItem Variant Attribute` t1
WHERE
1 = 1
AND (
{attribute_query}
)
AND EXISTS (
SELECT
1
FROM
`tabItem` t2
WHERE
t2.name = t1.parent
{variant_of_query}
)
GROUP BY
t1.parent
ORDER BY
NULL
'''.format(attribute_query=attribute_query, variant_of_query=variant_of_query)
item_codes = set([r[0] for r in frappe.db.sql(query, query_values)])
items.append(item_codes)
res = list(set.intersection(*items))
return res
@frappe.whitelist(allow_guest=True)
def get_attributes_and_values(item_code):
'''Build a list of attributes and their possible values.
This will ignore the values upon selection of which there cannot exist one item.
'''
item_cache = ItemVariantsCacheManager(item_code)
item_variants_data = item_cache.get_item_variants_data()
attributes = get_item_attributes(item_code)
attribute_list = [a.attribute for a in attributes]
valid_options = {}
for item_code, attribute, attribute_value in item_variants_data:
if attribute in attribute_list:
valid_options.setdefault(attribute, set()).add(attribute_value)
item_attribute_values = frappe.db.get_all('Item Attribute Value',
['parent', 'attribute_value', 'idx'], order_by='parent asc, idx asc')
ordered_attribute_value_map = frappe._dict()
for iv in item_attribute_values:
ordered_attribute_value_map.setdefault(iv.parent, []).append(iv.attribute_value)
# build attribute values in idx order
for attr in attributes:
valid_attribute_values = valid_options.get(attr.attribute, [])
ordered_values = ordered_attribute_value_map.get(attr.attribute, [])
attr['values'] = [v for v in ordered_values if v in valid_attribute_values]
return attributes
@frappe.whitelist(allow_guest=True)
def get_next_attribute_and_values(item_code, selected_attributes):
'''Find the count of Items that match the selected attributes.
Also, find the attribute values that are not applicable for further searching.
If less than equal to 10 items are found, return item_codes of those items.
If one item is matched exactly, return item_code of that item.
'''
selected_attributes = frappe.parse_json(selected_attributes)
item_cache = ItemVariantsCacheManager(item_code)
item_variants_data = item_cache.get_item_variants_data()
attributes = get_item_attributes(item_code)
attribute_list = [a.attribute for a in attributes]
filtered_items = get_items_with_selected_attributes(item_code, selected_attributes)
next_attribute = None
for attribute in attribute_list:
if attribute not in selected_attributes:
next_attribute = attribute
break
valid_options_for_attributes = frappe._dict()
for a in attribute_list:
valid_options_for_attributes[a] = set()
selected_attribute = selected_attributes.get(a, None)
if selected_attribute:
# already selected attribute values are valid options
valid_options_for_attributes[a].add(selected_attribute)
for row in item_variants_data:
item_code, attribute, attribute_value = row
if item_code in filtered_items and attribute not in selected_attributes and attribute in attribute_list:
valid_options_for_attributes[attribute].add(attribute_value)
optional_attributes = item_cache.get_optional_attributes()
exact_match = []
# search for exact match if all selected attributes are required attributes
if len(selected_attributes.keys()) >= (len(attribute_list) - len(optional_attributes)):
item_attribute_value_map = item_cache.get_item_attribute_value_map()
for item_code, attr_dict in item_attribute_value_map.items():
if item_code in filtered_items and set(attr_dict.keys()) == set(selected_attributes.keys()):
exact_match.append(item_code)
filtered_items_count = len(filtered_items)
# get product info if exact match
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
if exact_match:
data = get_product_info_for_website(exact_match[0])
product_info = data.product_info
if product_info:
product_info["allow_items_not_in_stock"] = cint(data.cart_settings.allow_items_not_in_stock)
if not data.cart_settings.show_price:
product_info = None
else:
product_info = None
return {
'next_attribute': next_attribute,
'valid_options_for_attributes': valid_options_for_attributes,
'filtered_items_count': filtered_items_count,
'filtered_items': filtered_items if filtered_items_count < 10 else [],
'exact_match': exact_match,
'product_info': product_info
}
def get_items_with_selected_attributes(item_code, selected_attributes):
item_cache = ItemVariantsCacheManager(item_code)
attribute_value_item_map = item_cache.get_attribute_value_item_map()
items = []
for attribute, value in selected_attributes.items():
filtered_items = attribute_value_item_map.get((attribute, value), [])
items.append(set(filtered_items))
return set.intersection(*items)
# utilities
def get_item_attributes(item_code):
attributes = frappe.db.get_all('Item Variant Attribute',
fields=['attribute'],
filters={
'parenttype': 'Item',
'parent': item_code
},
order_by='idx asc'
)
optional_attributes = ItemVariantsCacheManager(item_code).get_optional_attributes()
for a in attributes:
if a.attribute in optional_attributes:
a.optional = True
return attributes

View File

@@ -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,

View File

@@ -25,9 +25,8 @@
{%- if item -%}
{%- set item = frappe.get_doc("Item", item) -%}
{{ item_card(
item.item_name, item.image, item.route, item.description,
None, item.item_group, values['card_' + index + '_featured'],
True, "Center"
item, is_featured=values['card_' + index + '_featured'],
is_full_width=True, align="Center"
) }}
{%- endif -%}
{%- endfor -%}

View File

@@ -17,15 +17,12 @@
"reqd": 0
},
{
"__unsaved": 1,
"fieldname": "primary_action_label",
"fieldtype": "Data",
"label": "Primary Action Label",
"reqd": 0
},
{
"__islocal": 1,
"__unsaved": 1,
"fieldname": "primary_action",
"fieldtype": "Data",
"label": "Primary Action",
@@ -262,9 +259,9 @@
}
],
"idx": 0,
"modified": "2020-11-19 18:48:52.633045",
"modified": "2021-02-24 16:05:31.242342",
"modified_by": "Administrator",
"module": "Shopping Cart",
"module": "E-commerce",
"name": "Item Card Group",
"owner": "Administrator",
"standard": 1,

View File

@@ -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,

View File

@@ -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>

View File

@@ -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,

View File

@@ -143,7 +143,6 @@ def create_item_code(amazon_item_json, sku):
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

View File

@@ -51,15 +51,15 @@ additional_print_settings = "erpnext.controllers.print_settings.get_print_settin
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"
calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "Course Schedule"]
@@ -75,7 +75,7 @@ domains = {
'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 = {
@@ -249,10 +249,7 @@ doc_events = {
]
},
"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"

View File

@@ -8,7 +8,6 @@ from frappe.model.document import Document
from frappe.utils import add_years, now, get_datetime, get_datetime_str, cint
from frappe import _
from frappe.frappeclient import FrappeClient
from erpnext.utilities.product import get_price, get_qty_in_stock
from six import string_types
class MarketplaceSettings(Document):

View File

@@ -9,7 +9,6 @@ Manufacturing
Stock
Support
Utilities
Shopping Cart
Assets
Portal
Maintenance
@@ -27,3 +26,4 @@ Communication
Loan Management
Payroll
Telephony
E-commerce

View File

@@ -308,3 +308,6 @@ erpnext.patches.v13_0.migrate_stripe_api
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
erpnext.patches.v13_0.validate_options_for_data_field
erpnext.patches.v13_0.create_website_items
erpnext.patches.v13_0.populate_e_commerce_settings
erpnext.patches.v13_0.make_homepage_products_website_items

View File

@@ -0,0 +1,70 @@
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("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"]
item_table_fields = frappe.db.sql("desc `tabItem`", as_dict=1)
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
)
count = 0
for item in items:
if frappe.db.exists("Website Item", {"item_code": item.item_code}):
continue
# make website item from item (publish item)
website_item = make_website_item(item, save=False)
website_item.ranking = item.get("weightage")
for field in web_fields_to_map:
website_item.update({field: item.get(field)})
website_item.save()
# move Website Item Group & Website Specification table to Website Item
for doctype in ("Website Item Group", "Item Website Specification"):
web_item, item_code = website_item.name, item.item_code
frappe.db.sql(f"""
Update
`tab{doctype}`
set
parenttype = 'Website Item',
parent = '{web_item}'
where
parenttype = 'Item'
and parent = '{item_code}'
""")
count += 1
if count % 20 == 0: # commit after every 20 items
frappe.db.commit()

View File

@@ -0,0 +1,15 @@
from __future__ import unicode_literals
import frappe
def execute():
homepage = frappe.get_doc("Homepage")
for row in homepage.products:
web_item = frappe.db.get_value("Website Item", {"item_code": row.item_code}, "name")
if not web_item:
continue
row.item_code = web_item
homepage.flags.ignore_mandatory = True
homepage.save()

View File

@@ -0,0 +1,60 @@
from __future__ import unicode_literals
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):
data = frappe.db.sql("""
Select
field, value
from `tabSingles`
where
doctype='{doctype}'
and field in ({fields})
""".format(
doctype=doctype,
fields=(",").join(['%s'] * len(fields))
), tuple(fields), as_dict=1)
# {'enable_attribute_filters': '1', ...}
mapper = {row.field: row.value for row in data}
for key, value in mapper.items():
value = cint(value) if (value and value.isdigit()) else value
settings.update({key: value})
settings.save()
# shift data to E Commerce Settings
map_into_e_commerce_settings("Products Settings", products_settings_fields)
map_into_e_commerce_settings("Shopping Cart Settings", shopping_cart_settings_fields)
# move filters and attributes tables to E Commerce Settings from Products Settings
for doctype in ("Website Filter Field", "Website Attribute"):
frappe.db.sql("""Update `tab{doctype}`
set
parenttype = 'E Commerce Settings',
parent = 'E Commerce Settings'
where
parent = 'Products Settings'
""".format(doctype=doctype))

View File

@@ -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', {
});
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);
var child= locals[cdt][cdn];
if (child.item_code && child.route) {
window.open('/' + child.route, '_blank');
}
}
});

View File

@@ -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
}

View File

@@ -14,12 +14,14 @@ class Homepage(Document):
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))

View File

@@ -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",

View File

@@ -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
);
});
}
});

View File

@@ -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
}

View File

@@ -1,43 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.utils import cint
from frappe import _
from frappe.model.document import Document
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'

View File

@@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
class TestProductsSettings(unittest.TestCase):
pass

View File

@@ -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,
"actions": [],
"creation": "2019-01-01 13:04:54.479079",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"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
"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",
"links": [],
"modified": "2021-02-18 13:18:57.810536",
"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
"track_changes": 1
}

View File

@@ -1,144 +0,0 @@
from __future__ import unicode_literals
from bs4 import BeautifulSoup
import frappe, unittest
from frappe.utils import set_request, get_html_for_route
from frappe.website.render import render
from erpnext.portal.product_configurator.utils import get_products_for_website
from erpnext.stock.doctype.item.test_item import make_item_variant
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()

View File

@@ -1,444 +0,0 @@
import frappe
from frappe.utils import cint
from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager
from erpnext.shopping_cart.product_info import get_product_info_for_website
from erpnext.setup.doctype.item_group.item_group import get_child_groups
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

View File

@@ -1,7 +1,7 @@
from __future__ import unicode_literals
import frappe
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import get_shopping_cart_settings
from erpnext.shopping_cart.cart import get_debtors_account
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
from frappe.utils.nestedset import get_root_of
def set_default_role(doc, method):

View File

@@ -11,7 +11,8 @@
],
"js/erpnext-web.min.js": [
"public/js/website_utils.js",
"public/js/shopping_cart.js"
"public/js/shopping_cart.js",
"public/js/wishlist.js"
],
"css/erpnext-web.css": [
"public/scss/website.scss",
@@ -69,6 +70,12 @@
"public/js/bank_reconciliation_tool/data_table_manager.js",
"public/js/bank_reconciliation_tool/number_card.js",
"public/js/bank_reconciliation_tool/dialog_manager.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"
],
"js/hierarchy-chart.min.js": [
"public/js/hierarchy_chart/hierarchy_chart_desktop.js",

View File

@@ -21,6 +21,6 @@ $.extend(frappe.breadcrumbs.module_map, {
'Geo': 'Settings',
'Portal': 'Website',
'Utilities': 'Settings',
'Shopping Cart': 'Website',
'E-commerce': 'Website',
'Contacts': 'CRM'
});

View File

@@ -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 @@ frappe.ready(function() {
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 @@ $.extend(shopping_cart, {
$(".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);
@@ -79,11 +79,14 @@ $.extend(shopping_cart, {
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 @@ $.extend(shopping_cart, {
},
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,7 @@ $.extend(shopping_cart, {
}
},
set_cart_count: function() {
set_cart_count: function(animate=false) {
var cart_count = frappe.get_cookie("cart_count");
if(frappe.session.user==="Guest") {
cart_count = 0;
@@ -121,7 +122,7 @@ $.extend(shopping_cart, {
$(".cart-items").html('Cart is Empty');
$(".cart-tax-items").hide();
$(".btn-place-order").hide();
$(".cart-addresses").hide();
$(".cart-payment-addresses").hide();
}
else {
$cart.css("display", "inline");
@@ -129,13 +130,19 @@ $.extend(shopping_cart, {
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,7 +150,6 @@ $.extend(shopping_cart, {
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);
@@ -155,35 +161,71 @@ $.extend(shopping_cart, {
});
},
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);
}
}
});

View File

@@ -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();
}
});

Some files were not shown because too many files have changed in this diff Show More