Merge pull request #27923 from marination/e-commerce-refactor-develop
refactor: E-commerce (port to develop)
This commit is contained in:
commit
551f2967da
@ -39,9 +39,6 @@ def test_create_test_data():
|
|||||||
"selling_cost_center": "Main - _TC",
|
"selling_cost_center": "Main - _TC",
|
||||||
"income_account": "Sales - _TC"
|
"income_account": "Sales - _TC"
|
||||||
}],
|
}],
|
||||||
"show_in_website": 1,
|
|
||||||
"route":"-test-tesla-car",
|
|
||||||
"website_warehouse": "Stores - _TC"
|
|
||||||
})
|
})
|
||||||
item.insert()
|
item.insert()
|
||||||
# create test item price
|
# create test item price
|
||||||
|
@ -291,7 +291,7 @@ class PaymentRequest(Document):
|
|||||||
if not status:
|
if not status:
|
||||||
return
|
return
|
||||||
|
|
||||||
shopping_cart_settings = frappe.get_doc("Shopping Cart Settings")
|
shopping_cart_settings = frappe.get_doc("E Commerce Settings")
|
||||||
|
|
||||||
if status in ["Authorized", "Completed"]:
|
if status in ["Authorized", "Completed"]:
|
||||||
redirect_to = None
|
redirect_to = None
|
||||||
@ -435,13 +435,13 @@ def get_existing_payment_request_amount(ref_dt, ref_dn):
|
|||||||
""", (ref_dt, ref_dn))
|
""", (ref_dt, ref_dn))
|
||||||
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
|
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
|
||||||
|
|
||||||
def get_gateway_details(args):
|
def get_gateway_details(args): # nosemgrep
|
||||||
"""return gateway and payment account of default payment gateway"""
|
"""return gateway and payment account of default payment gateway"""
|
||||||
if args.get("payment_gateway_account"):
|
if args.get("payment_gateway_account"):
|
||||||
return get_payment_gateway_account(args.get("payment_gateway_account"))
|
return get_payment_gateway_account(args.get("payment_gateway_account"))
|
||||||
|
|
||||||
if args.order_type == "Shopping Cart":
|
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)
|
return get_payment_gateway_account(payment_gateway_account)
|
||||||
|
|
||||||
gateway_account = get_payment_gateway_account({"is_default": 1})
|
gateway_account = get_payment_gateway_account({"is_default": 1})
|
||||||
|
@ -98,7 +98,7 @@ class TaxRule(Document):
|
|||||||
def validate_use_for_shopping_cart(self):
|
def validate_use_for_shopping_cart(self):
|
||||||
'''If shopping cart is enabled and no tax rule exists for shopping cart, enable this one'''
|
'''If shopping cart is enabled and no tax rule exists for shopping cart, enable this one'''
|
||||||
if (not self.use_for_shopping_cart
|
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]})):
|
and not frappe.db.get_value('Tax Rule', {'use_for_shopping_cart': 1, 'name': ['!=', self.name]})):
|
||||||
|
|
||||||
self.use_for_shopping_cart = 1
|
self.use_for_shopping_cart = 1
|
||||||
|
@ -131,28 +131,6 @@ class Supplier(TransactionBase):
|
|||||||
if frappe.defaults.get_global_default('supp_master_name') == 'Supplier Name':
|
if frappe.defaults.get_global_default('supp_master_name') == 'Supplier Name':
|
||||||
frappe.db.set(self, "supplier_name", newdn)
|
frappe.db.set(self, "supplier_name", newdn)
|
||||||
|
|
||||||
def create_onboarding_docs(self, args):
|
|
||||||
company = frappe.defaults.get_defaults().get('company') or \
|
|
||||||
frappe.db.get_single_value('Global Defaults', 'default_company')
|
|
||||||
|
|
||||||
for i in range(1, args.get('max_count')):
|
|
||||||
supplier = args.get('supplier_name_' + str(i))
|
|
||||||
if supplier:
|
|
||||||
try:
|
|
||||||
doc = frappe.get_doc({
|
|
||||||
'doctype': self.doctype,
|
|
||||||
'supplier_name': supplier,
|
|
||||||
'supplier_group': _('Local'),
|
|
||||||
'company': company
|
|
||||||
}).insert()
|
|
||||||
|
|
||||||
if args.get('supplier_email_' + str(i)):
|
|
||||||
from erpnext.selling.doctype.customer.customer import create_contact
|
|
||||||
create_contact(supplier, 'Supplier',
|
|
||||||
doc.name, args.get('supplier_email_' + str(i)))
|
|
||||||
except frappe.NameError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters):
|
def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters):
|
||||||
|
@ -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"
|
|
||||||
}
|
|
@ -132,7 +132,7 @@ def find_variant(template, args, variant_item_code=None):
|
|||||||
|
|
||||||
conditions = " or ".join(conditions)
|
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]
|
possible_variants = [i for i in get_item_codes_by_attributes(args, template) if i != variant_item_code]
|
||||||
|
|
||||||
for variant in possible_variants:
|
for variant in possible_variants:
|
||||||
@ -262,9 +262,8 @@ def generate_keyed_value_combinations(args):
|
|||||||
def copy_attributes_to_variant(item, variant):
|
def copy_attributes_to_variant(item, variant):
|
||||||
# copy non no-copy fields
|
# copy non no-copy fields
|
||||||
|
|
||||||
exclude_fields = ["naming_series", "item_code", "item_name", "show_in_website",
|
exclude_fields = ["naming_series", "item_code", "item_name", "published_in_website",
|
||||||
"show_variant_in_website", "opening_stock", "variant_of", "valuation_rate",
|
"opening_stock", "variant_of", "valuation_rate"]
|
||||||
"has_variants", "attributes"]
|
|
||||||
|
|
||||||
if item.variant_based_on=='Manufacturer':
|
if item.variant_based_on=='Manufacturer':
|
||||||
# don't copy manufacturer values if based on part no
|
# don't copy manufacturer values if based on part no
|
||||||
|
86
erpnext/e_commerce/api.py
Normal file
86
erpnext/e_commerce/api.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.utils import cint
|
||||||
|
|
||||||
|
from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder
|
||||||
|
from erpnext.e_commerce.product_data_engine.query import ProductQuery
|
||||||
|
from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def get_product_filter_data(query_args=None):
|
||||||
|
"""
|
||||||
|
Returns filtered products and discount filters.
|
||||||
|
:param query_args (dict): contains filters to get products list
|
||||||
|
|
||||||
|
Query Args filters:
|
||||||
|
search (str): Search Term.
|
||||||
|
field_filters (dict): Keys include item_group, brand, etc.
|
||||||
|
attribute_filters(dict): Keys include Color, Size, etc.
|
||||||
|
start (int): Offset items by
|
||||||
|
item_group (str): Valid Item Group
|
||||||
|
from_filters (bool): Set as True to jump to page 1
|
||||||
|
"""
|
||||||
|
if isinstance(query_args, str):
|
||||||
|
query_args = json.loads(query_args)
|
||||||
|
|
||||||
|
query_args = frappe._dict(query_args)
|
||||||
|
if query_args:
|
||||||
|
search = query_args.get("search")
|
||||||
|
field_filters = query_args.get("field_filters", {})
|
||||||
|
attribute_filters = query_args.get("attribute_filters", {})
|
||||||
|
start = cint(query_args.start) if query_args.get("start") else 0
|
||||||
|
item_group = query_args.get("item_group")
|
||||||
|
from_filters = query_args.get("from_filters")
|
||||||
|
else:
|
||||||
|
search, attribute_filters, item_group, from_filters = None, None, None, None
|
||||||
|
field_filters = {}
|
||||||
|
start = 0
|
||||||
|
|
||||||
|
# if new filter is checked, reset start to show filtered items from page 1
|
||||||
|
if from_filters:
|
||||||
|
start = 0
|
||||||
|
|
||||||
|
sub_categories = []
|
||||||
|
if item_group:
|
||||||
|
field_filters['item_group'] = item_group
|
||||||
|
sub_categories = get_child_groups_for_website(item_group, immediate=True)
|
||||||
|
|
||||||
|
engine = ProductQuery()
|
||||||
|
try:
|
||||||
|
result = engine.query(
|
||||||
|
attribute_filters,
|
||||||
|
field_filters,
|
||||||
|
search_term=search,
|
||||||
|
start=start,
|
||||||
|
item_group=item_group
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
traceback = frappe.get_traceback()
|
||||||
|
frappe.log_error(traceback, frappe._("Product Engine Error"))
|
||||||
|
return {"exc": "Something went wrong!"}
|
||||||
|
|
||||||
|
# discount filter data
|
||||||
|
filters = {}
|
||||||
|
discounts = result["discounts"]
|
||||||
|
|
||||||
|
if discounts:
|
||||||
|
filter_engine = ProductFiltersBuilder()
|
||||||
|
filters["discount_filters"] = filter_engine.get_discount_filters(discounts)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"items": result["items"] or [],
|
||||||
|
"filters": filters,
|
||||||
|
"settings": engine.settings,
|
||||||
|
"sub_categories": sub_categories,
|
||||||
|
"items_count": result["items_count"]
|
||||||
|
}
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def get_guest_redirect_on_action():
|
||||||
|
return frappe.db.get_single_value("E Commerce Settings", "redirect_on_action")
|
@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
// License: GNU General Public License v3. See license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on("Shopping Cart Settings", {
|
frappe.ui.form.on("E Commerce Settings", {
|
||||||
onload: function(frm) {
|
onload: function(frm) {
|
||||||
if(frm.doc.__onload && frm.doc.__onload.quotation_series) {
|
if(frm.doc.__onload && frm.doc.__onload.quotation_series) {
|
||||||
frm.fields_dict.quotation_series.df.options = 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>`
|
</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) {
|
enabled: function(frm) {
|
||||||
if (frm.doc.enabled === 1) {
|
if (frm.doc.enabled === 1) {
|
@ -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
|
||||||
|
}
|
@ -1,25 +1,81 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# -*- coding: utf-8 -*-
|
||||||
# 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
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import flt
|
from frappe.utils import comma_and, flt, unique
|
||||||
|
|
||||||
|
from erpnext.e_commerce.redisearch_utils import (
|
||||||
|
create_website_items_index,
|
||||||
|
get_indexable_web_fields,
|
||||||
|
is_search_module_loaded,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ShoppingCartSetupError(frappe.ValidationError): pass
|
class ShoppingCartSetupError(frappe.ValidationError): pass
|
||||||
|
|
||||||
class ShoppingCartSettings(Document):
|
class ECommerceSettings(Document):
|
||||||
def onload(self):
|
def onload(self):
|
||||||
self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series")
|
self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series")
|
||||||
|
self.is_redisearch_loaded = is_search_module_loaded()
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
self.validate_field_filters()
|
||||||
|
self.validate_attribute_filters()
|
||||||
|
self.validate_checkout()
|
||||||
|
self.validate_search_index_fields()
|
||||||
|
|
||||||
if self.enabled:
|
if self.enabled:
|
||||||
self.validate_price_list_exchange_rate()
|
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):
|
def validate_price_list_exchange_rate(self):
|
||||||
"Check if exchange rate exists for Price List currency (to Company's currency)."
|
"Check if exchange rate exists for Price List currency (to Company's currency)."
|
||||||
from erpnext.setup.utils import get_exchange_rate
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
@ -60,12 +116,23 @@ class ShoppingCartSettings(Document):
|
|||||||
def get_shipping_rules(self, shipping_territory):
|
def get_shipping_rules(self, shipping_territory):
|
||||||
return self.get_name_from_territory(shipping_territory, "shipping_rules", "shipping_rule")
|
return self.get_name_from_territory(shipping_territory, "shipping_rules", "shipping_rule")
|
||||||
|
|
||||||
|
def on_change(self):
|
||||||
|
old_doc = self.get_doc_before_save()
|
||||||
|
|
||||||
|
if old_doc:
|
||||||
|
old_fields = old_doc.search_index_fields
|
||||||
|
new_fields = self.search_index_fields
|
||||||
|
|
||||||
|
# if search index fields get changed
|
||||||
|
if not (new_fields == old_fields):
|
||||||
|
create_website_items_index()
|
||||||
|
|
||||||
def validate_cart_settings(doc=None, method=None):
|
def validate_cart_settings(doc=None, method=None):
|
||||||
frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings").run_method("validate")
|
frappe.get_doc("E Commerce Settings", "E Commerce Settings").run_method("validate")
|
||||||
|
|
||||||
def get_shopping_cart_settings():
|
def get_shopping_cart_settings():
|
||||||
if not getattr(frappe.local, "shopping_cart_settings", None):
|
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
|
return frappe.local.shopping_cart_settings
|
||||||
|
|
@ -1,24 +1,21 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# -*- coding: utf-8 -*-
|
||||||
# License: GNU General Public License v3. See license.txt
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
|
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||||
ShoppingCartSetupError,
|
ShoppingCartSetupError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestShoppingCartSettings(unittest.TestCase):
|
class TestECommerceSettings(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
frappe.db.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """)
|
frappe.db.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """)
|
||||||
|
|
||||||
def get_cart_settings(self):
|
def get_cart_settings(self):
|
||||||
return frappe.get_doc({"doctype": "Shopping Cart Settings",
|
return frappe.get_doc({"doctype": "E Commerce Settings",
|
||||||
"company": "_Test Company"})
|
"company": "_Test Company"})
|
||||||
|
|
||||||
# NOTE: Exchangrate API has all enabled currencies that ERPNext supports.
|
# NOTE: Exchangrate API has all enabled currencies that ERPNext supports.
|
||||||
@ -34,15 +31,17 @@ class TestShoppingCartSettings(unittest.TestCase):
|
|||||||
|
|
||||||
# cart_settings = self.get_cart_settings()
|
# cart_settings = self.get_cart_settings()
|
||||||
# cart_settings.price_list = "_Test Price List Rest of the World"
|
# cart_settings.price_list = "_Test Price List Rest of the World"
|
||||||
# self.assertRaises(ShoppingCartSetupError, cart_settings.validate_price_list_exchange_rate)
|
# self.assertRaises(ShoppingCartSetupError, cart_settings.validate_exchange_rates_exist)
|
||||||
|
|
||||||
# from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \
|
# from erpnext.setup.doctype.currency_exchange.test_currency_exchange import (
|
||||||
# currency_exchange_records
|
# test_records as currency_exchange_records,
|
||||||
|
# )
|
||||||
# frappe.get_doc(currency_exchange_records[0]).insert()
|
# frappe.get_doc(currency_exchange_records[0]).insert()
|
||||||
# cart_settings.validate_price_list_exchange_rate()
|
# cart_settings.validate_exchange_rates_exist()
|
||||||
|
|
||||||
def test_tax_rule_validation(self):
|
def test_tax_rule_validation(self):
|
||||||
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
|
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
|
||||||
|
frappe.db.commit() # nosemgrep
|
||||||
|
|
||||||
cart_settings = self.get_cart_settings()
|
cart_settings = self.get_cart_settings()
|
||||||
cart_settings.enabled = 1
|
cart_settings.enabled = 1
|
||||||
@ -51,4 +50,13 @@ class TestShoppingCartSettings(unittest.TestCase):
|
|||||||
|
|
||||||
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1")
|
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"]
|
test_dependencies = ["Tax Rule"]
|
8
erpnext/e_commerce/doctype/item_review/item_review.js
Normal file
8
erpnext/e_commerce/doctype/item_review/item_review.js
Normal 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) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
});
|
134
erpnext/e_commerce/doctype/item_review/item_review.json
Normal file
134
erpnext/e_commerce/doctype/item_review/item_review.json
Normal 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
|
||||||
|
}
|
147
erpnext/e_commerce/doctype/item_review/item_review.py
Normal file
147
erpnext/e_commerce/doctype/item_review/item_review.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.contacts.doctype.contact.contact import get_contact_name
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from frappe.utils import cint, flt
|
||||||
|
|
||||||
|
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||||
|
get_shopping_cart_settings,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UnverifiedReviewer(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ItemReview(Document):
|
||||||
|
def after_insert(self):
|
||||||
|
# regenerate cache on review creation
|
||||||
|
reviews_dict = get_queried_reviews(self.website_item)
|
||||||
|
set_reviews_in_cache(self.website_item, reviews_dict)
|
||||||
|
|
||||||
|
def after_delete(self):
|
||||||
|
# regenerate cache on review deletion
|
||||||
|
reviews_dict = get_queried_reviews(self.website_item)
|
||||||
|
set_reviews_in_cache(self.website_item, reviews_dict)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_item_reviews(web_item, start=0, end=10, data=None):
|
||||||
|
"Get Website Item Review Data."
|
||||||
|
start, end = cint(start), cint(end)
|
||||||
|
settings = get_shopping_cart_settings()
|
||||||
|
|
||||||
|
# Get cached reviews for first page (start=0)
|
||||||
|
# avoid cache when page is different
|
||||||
|
from_cache = not bool(start)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
data = frappe._dict()
|
||||||
|
|
||||||
|
if settings and settings.get("enable_reviews"):
|
||||||
|
reviews_cache = frappe.cache().hget("item_reviews", web_item)
|
||||||
|
if from_cache and reviews_cache:
|
||||||
|
data = reviews_cache
|
||||||
|
else:
|
||||||
|
data = get_queried_reviews(web_item, start, end, data)
|
||||||
|
if from_cache:
|
||||||
|
set_reviews_in_cache(web_item, data)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_queried_reviews(web_item, start=0, end=10, data=None):
|
||||||
|
"""
|
||||||
|
Query Website Item wise reviews and cache if needed.
|
||||||
|
Cache stores only first page of reviews i.e. 10 reviews maximum.
|
||||||
|
Returns:
|
||||||
|
dict: Containing reviews, average ratings, % of reviews per rating and total reviews.
|
||||||
|
"""
|
||||||
|
if not data:
|
||||||
|
data = frappe._dict()
|
||||||
|
|
||||||
|
data.reviews = frappe.db.get_all(
|
||||||
|
"Item Review",
|
||||||
|
filters={"website_item": web_item},
|
||||||
|
fields=["*"],
|
||||||
|
limit_start=start,
|
||||||
|
limit_page_length=end
|
||||||
|
)
|
||||||
|
|
||||||
|
rating_data = frappe.db.get_all(
|
||||||
|
"Item Review",
|
||||||
|
filters={"website_item": web_item},
|
||||||
|
fields=["avg(rating) as average, count(*) as total"]
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
data.average_rating = flt(rating_data.average, 1)
|
||||||
|
data.average_whole_rating = flt(data.average_rating, 0)
|
||||||
|
|
||||||
|
# get % of reviews per rating
|
||||||
|
reviews_per_rating = []
|
||||||
|
for i in range(1,6):
|
||||||
|
count = frappe.db.get_all(
|
||||||
|
"Item Review",
|
||||||
|
filters={"website_item": web_item, "rating": i},
|
||||||
|
fields=["count(*) as count"]
|
||||||
|
)[0].count
|
||||||
|
|
||||||
|
percent = flt((count / rating_data.total or 1) * 100, 0) if count else 0
|
||||||
|
reviews_per_rating.append(percent)
|
||||||
|
|
||||||
|
data.reviews_per_rating = reviews_per_rating
|
||||||
|
data.total_reviews = rating_data.total
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def set_reviews_in_cache(web_item, reviews_dict):
|
||||||
|
frappe.cache().hset("item_reviews", web_item, reviews_dict)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def add_item_review(web_item, title, rating, comment=None):
|
||||||
|
""" Add an Item Review by a user if non-existent. """
|
||||||
|
if frappe.session.user == "Guest":
|
||||||
|
# guest user should not reach here ideally in the case they do via an API, throw error
|
||||||
|
frappe.throw(_("You are not verified to write a review yet."), exc=UnverifiedReviewer)
|
||||||
|
|
||||||
|
if not frappe.db.exists("Item Review", {"user": frappe.session.user, "website_item": web_item}):
|
||||||
|
doc = frappe.get_doc({
|
||||||
|
"doctype": "Item Review",
|
||||||
|
"user": frappe.session.user,
|
||||||
|
"customer": get_customer(),
|
||||||
|
"website_item": web_item,
|
||||||
|
"item": frappe.db.get_value("Website Item", web_item, "item_code"),
|
||||||
|
"review_title": title,
|
||||||
|
"rating": rating,
|
||||||
|
"comment": comment
|
||||||
|
})
|
||||||
|
doc.published_on = datetime.today().strftime("%d %B %Y")
|
||||||
|
doc.insert()
|
||||||
|
|
||||||
|
def get_customer(silent=False):
|
||||||
|
"""
|
||||||
|
silent: Return customer if exists else return nothing. Dont throw error.
|
||||||
|
"""
|
||||||
|
user = frappe.session.user
|
||||||
|
contact_name = get_contact_name(user)
|
||||||
|
customer = None
|
||||||
|
|
||||||
|
if contact_name:
|
||||||
|
contact = frappe.get_doc('Contact', contact_name)
|
||||||
|
for link in contact.links:
|
||||||
|
if link.link_doctype == "Customer":
|
||||||
|
customer = link.link_name
|
||||||
|
break
|
||||||
|
|
||||||
|
if customer:
|
||||||
|
return frappe.db.get_value("Customer", customer)
|
||||||
|
elif silent:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
# should not reach here unless via an API
|
||||||
|
frappe.throw(_("You are not a verified customer yet. Please contact us to proceed."),
|
||||||
|
exc=UnverifiedReviewer)
|
84
erpnext/e_commerce/doctype/item_review/test_item_review.py
Normal file
84
erpnext/e_commerce/doctype/item_review/test_item_review.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
||||||
|
|
||||||
|
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
|
||||||
|
setup_e_commerce_settings,
|
||||||
|
)
|
||||||
|
from erpnext.e_commerce.doctype.item_review.item_review import (
|
||||||
|
UnverifiedReviewer,
|
||||||
|
add_item_review,
|
||||||
|
get_item_reviews,
|
||||||
|
)
|
||||||
|
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||||
|
from erpnext.e_commerce.shopping_cart.cart import get_party
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
|
|
||||||
|
class TestItemReview(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
item = make_item("Test Mobile Phone")
|
||||||
|
if not frappe.db.exists("Website Item", {"item_code": "Test Mobile Phone"}):
|
||||||
|
make_website_item(item, save=True)
|
||||||
|
|
||||||
|
setup_e_commerce_settings({"enable_reviews": 1})
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.get_cached_doc("Website Item", {"item_code": "Test Mobile Phone"}).delete()
|
||||||
|
setup_e_commerce_settings({"enable_reviews": 0})
|
||||||
|
|
||||||
|
def test_add_and_get_item_reviews_from_customer(self):
|
||||||
|
"Add / Get Reviews from a User that is a valid customer (has added to cart or purchased in the past)"
|
||||||
|
# create user
|
||||||
|
web_item = frappe.db.get_value("Website Item", {"item_code": "Test Mobile Phone"})
|
||||||
|
test_user = create_user("test_reviewer@example.com", "Customer")
|
||||||
|
frappe.set_user(test_user.name)
|
||||||
|
|
||||||
|
# create customer and contact against user
|
||||||
|
customer = get_party()
|
||||||
|
|
||||||
|
# post review on "Test Mobile Phone"
|
||||||
|
try:
|
||||||
|
add_item_review(web_item, "Great Product", 3, "Would recommend this product")
|
||||||
|
review_name = frappe.db.get_value("Item Review", {"website_item": web_item})
|
||||||
|
except Exception:
|
||||||
|
self.fail(f"Error while publishing review for {web_item}")
|
||||||
|
|
||||||
|
review_data = get_item_reviews(web_item, 0, 10)
|
||||||
|
|
||||||
|
self.assertEqual(len(review_data.reviews), 1)
|
||||||
|
self.assertEqual(review_data.average_rating, 3)
|
||||||
|
self.assertEqual(review_data.reviews_per_rating[2], 100)
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
frappe.delete_doc("Item Review", review_name)
|
||||||
|
customer.delete()
|
||||||
|
|
||||||
|
def test_add_item_review_from_non_customer(self):
|
||||||
|
"Check if logged in user (who is not a customer yet) is blocked from posting reviews."
|
||||||
|
web_item = frappe.db.get_value("Website Item", {"item_code": "Test Mobile Phone"})
|
||||||
|
test_user = create_user("test_reviewer@example.com", "Customer")
|
||||||
|
frappe.set_user(test_user.name)
|
||||||
|
|
||||||
|
with self.assertRaises(UnverifiedReviewer):
|
||||||
|
add_item_review(web_item, "Great Product", 3, "Would recommend this product")
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
|
def test_add_item_reviews_from_guest_user(self):
|
||||||
|
"Check if Guest user is blocked from posting reviews."
|
||||||
|
web_item = frappe.db.get_value("Website Item", {"item_code": "Test Mobile Phone"})
|
||||||
|
frappe.set_user("Guest")
|
||||||
|
|
||||||
|
with self.assertRaises(UnverifiedReviewer):
|
||||||
|
add_item_review(web_item, "Great Product", 3, "Would recommend this product")
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
frappe.set_user("Administrator")
|
@ -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
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class RecommendedItems(Document):
|
||||||
|
pass
|
@ -0,0 +1,7 @@
|
|||||||
|
{% extends "templates/web.html" %}
|
||||||
|
|
||||||
|
{% block page_content %}
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<!-- this is a sample default web page template -->
|
@ -0,0 +1,4 @@
|
|||||||
|
<div>
|
||||||
|
<a href="{{ doc.route }}">{{ doc.title or doc.name }}</a>
|
||||||
|
</div>
|
||||||
|
<!-- this is a sample default list template -->
|
538
erpnext/e_commerce/doctype/website_item/test_website_item.py
Normal file
538
erpnext/e_commerce/doctype/website_item/test_website_item.py
Normal file
@ -0,0 +1,538 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.controllers.item_variant import create_variant
|
||||||
|
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||||
|
get_shopping_cart_settings,
|
||||||
|
)
|
||||||
|
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
|
||||||
|
setup_e_commerce_settings,
|
||||||
|
)
|
||||||
|
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||||
|
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
|
||||||
|
from erpnext.stock.doctype.item.item import DataValidationError
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
|
WEBITEM_DESK_TESTS = ("test_website_item_desk_item_sync", "test_publish_variant_and_template")
|
||||||
|
WEBITEM_PRICE_TESTS = ('test_website_item_price_for_logged_in_user', 'test_website_item_price_for_guest_user')
|
||||||
|
|
||||||
|
class TestWebsiteItem(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
setup_e_commerce_settings({
|
||||||
|
"company": "_Test Company",
|
||||||
|
"enabled": 1,
|
||||||
|
"default_customer_group": "_Test Customer Group",
|
||||||
|
"price_list": "_Test Price List India"
|
||||||
|
})
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
if self._testMethodName in WEBITEM_DESK_TESTS:
|
||||||
|
make_item("Test Web Item", {
|
||||||
|
"has_variant": 1,
|
||||||
|
"variant_based_on": "Item Attribute",
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"attribute": "Test Size"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
elif self._testMethodName in WEBITEM_PRICE_TESTS:
|
||||||
|
create_user_and_customer_if_not_exists("test_contact_customer@example.com", "_Test Contact For _Test Customer")
|
||||||
|
create_regular_web_item()
|
||||||
|
make_web_item_price(item_code="Test Mobile Phone")
|
||||||
|
|
||||||
|
# Note: When testing web item pricing rule logged-in user pricing rule must differ from guest pricing rule or test will falsely pass.
|
||||||
|
# This is because make_web_pricing_rule creates a pricing rule "selling": 1, without specifying "applicable_for". Therefor,
|
||||||
|
# when testing for logged-in user the test will get the previous pricing rule because "selling" is still true.
|
||||||
|
#
|
||||||
|
# I've attempted to mitigate this by setting applicable_for=Customer, and customer=Guest however, this only results in PermissionError failing the test.
|
||||||
|
make_web_pricing_rule(
|
||||||
|
title="Test Pricing Rule for Test Mobile Phone",
|
||||||
|
item_code="Test Mobile Phone",
|
||||||
|
selling=1)
|
||||||
|
make_web_pricing_rule(
|
||||||
|
title="Test Pricing Rule for Test Mobile Phone (Customer)",
|
||||||
|
item_code="Test Mobile Phone",
|
||||||
|
selling=1,
|
||||||
|
discount_percentage="25",
|
||||||
|
applicable_for="Customer",
|
||||||
|
customer="_Test Customer")
|
||||||
|
|
||||||
|
def test_index_creation(self):
|
||||||
|
"Check if index is getting created in db."
|
||||||
|
from erpnext.e_commerce.doctype.website_item.website_item import on_doctype_update
|
||||||
|
on_doctype_update()
|
||||||
|
|
||||||
|
indices = frappe.db.sql("show index from `tabWebsite Item`", as_dict=1)
|
||||||
|
expected_columns = {"route", "item_group", "brand"}
|
||||||
|
for index in indices:
|
||||||
|
expected_columns.discard(index.get("Column_name"))
|
||||||
|
|
||||||
|
if expected_columns:
|
||||||
|
self.fail(f"Expected db index on these columns: {', '.join(expected_columns)}")
|
||||||
|
|
||||||
|
def test_website_item_desk_item_sync(self):
|
||||||
|
"Check creation/updation/deletion of Website Item and its impact on Item master."
|
||||||
|
web_item = None
|
||||||
|
item = make_item("Test Web Item") # will return item if exists
|
||||||
|
try:
|
||||||
|
web_item = make_website_item(item, save=False)
|
||||||
|
web_item.save()
|
||||||
|
except Exception:
|
||||||
|
self.fail(f"Error while creating website item for {item}")
|
||||||
|
|
||||||
|
# check if website item was created
|
||||||
|
self.assertTrue(bool(web_item))
|
||||||
|
self.assertTrue(bool(web_item.route))
|
||||||
|
|
||||||
|
item.reload()
|
||||||
|
self.assertEqual(web_item.published, 1)
|
||||||
|
self.assertEqual(item.published_in_website, 1) # check if item was back updated
|
||||||
|
self.assertEqual(web_item.item_group, item.item_group)
|
||||||
|
|
||||||
|
# check if changing item data changes it in website item
|
||||||
|
item.item_name = "Test Web Item 1"
|
||||||
|
item.stock_uom = "Unit"
|
||||||
|
item.save()
|
||||||
|
web_item.reload()
|
||||||
|
self.assertEqual(web_item.item_name, item.item_name)
|
||||||
|
self.assertEqual(web_item.stock_uom, item.stock_uom)
|
||||||
|
|
||||||
|
# check if disabling item unpublished website item
|
||||||
|
item.disabled = 1
|
||||||
|
item.save()
|
||||||
|
web_item.reload()
|
||||||
|
self.assertEqual(web_item.published, 0)
|
||||||
|
|
||||||
|
# check if website item deletion, unpublishes desk item
|
||||||
|
web_item.delete()
|
||||||
|
item.reload()
|
||||||
|
self.assertEqual(item.published_in_website, 0)
|
||||||
|
|
||||||
|
item.delete()
|
||||||
|
|
||||||
|
def test_publish_variant_and_template(self):
|
||||||
|
"Check if template is published on publishing variant."
|
||||||
|
# template "Test Web Item" created on setUp
|
||||||
|
variant = create_variant("Test Web Item", {"Test Size": "Large"})
|
||||||
|
variant.save()
|
||||||
|
|
||||||
|
# check if template is not published
|
||||||
|
self.assertIsNone(frappe.db.exists("Website Item", {"item_code": variant.variant_of}))
|
||||||
|
|
||||||
|
variant_web_item = make_website_item(variant, save=False)
|
||||||
|
variant_web_item.save()
|
||||||
|
|
||||||
|
# check if template is published
|
||||||
|
try:
|
||||||
|
template_web_item = frappe.get_doc("Website Item", {"item_code": variant.variant_of})
|
||||||
|
except frappe.DoesNotExistError:
|
||||||
|
self.fail(f"Template of {variant.item_code}, {variant.variant_of} not published")
|
||||||
|
|
||||||
|
# teardown
|
||||||
|
variant_web_item.delete()
|
||||||
|
template_web_item.delete()
|
||||||
|
variant.delete()
|
||||||
|
|
||||||
|
def test_impact_on_merging_items(self):
|
||||||
|
"Check if merging items is blocked if old and new items both have website items"
|
||||||
|
first_item = make_item("Test First Item")
|
||||||
|
second_item = make_item("Test Second Item")
|
||||||
|
|
||||||
|
first_web_item = make_website_item(first_item, save=False)
|
||||||
|
first_web_item.save()
|
||||||
|
second_web_item = make_website_item(second_item, save=False)
|
||||||
|
second_web_item.save()
|
||||||
|
|
||||||
|
with self.assertRaises(DataValidationError):
|
||||||
|
frappe.rename_doc("Item", "Test First Item", "Test Second Item", merge=True)
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
second_web_item.delete()
|
||||||
|
first_web_item.delete()
|
||||||
|
second_item.delete()
|
||||||
|
first_item.delete()
|
||||||
|
|
||||||
|
# Website Item Portal Tests Begin
|
||||||
|
|
||||||
|
def test_website_item_breadcrumbs(self):
|
||||||
|
"Check if breadcrumbs include homepage, product listing navigation page, parent item group(s) and item group."
|
||||||
|
from erpnext.setup.doctype.item_group.item_group import get_parent_item_groups
|
||||||
|
|
||||||
|
item_code = "Test Breadcrumb Item"
|
||||||
|
item = make_item(item_code, {
|
||||||
|
"item_group": "_Test Item Group B - 1",
|
||||||
|
})
|
||||||
|
|
||||||
|
if not frappe.db.exists("Website Item", {"item_code": item_code}):
|
||||||
|
web_item = make_website_item(item, save=False)
|
||||||
|
web_item.save()
|
||||||
|
else:
|
||||||
|
web_item = frappe.get_cached_doc("Website Item", {"item_code": item_code})
|
||||||
|
|
||||||
|
frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1)
|
||||||
|
frappe.db.set_value("Item Group", "_Test Item Group B", "show_in_website", 1)
|
||||||
|
|
||||||
|
breadcrumbs = get_parent_item_groups(item.item_group)
|
||||||
|
|
||||||
|
self.assertEqual(breadcrumbs[0]["name"], "Home")
|
||||||
|
self.assertEqual(breadcrumbs[1]["name"], "Shop by Category")
|
||||||
|
self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group
|
||||||
|
self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1")
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
web_item.delete()
|
||||||
|
item.delete()
|
||||||
|
|
||||||
|
def test_website_item_price_for_logged_in_user(self):
|
||||||
|
"Check if price details are fetched correctly while logged in."
|
||||||
|
item_code = "Test Mobile Phone"
|
||||||
|
|
||||||
|
# show price in e commerce settings
|
||||||
|
setup_e_commerce_settings({"show_price": 1})
|
||||||
|
|
||||||
|
# price and pricing rule added via setUp
|
||||||
|
|
||||||
|
# login as customer with pricing rule
|
||||||
|
frappe.set_user("test_contact_customer@example.com")
|
||||||
|
|
||||||
|
# check if price and slashed price is fetched correctly
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||||
|
self.assertTrue(bool(data.product_info["price"]))
|
||||||
|
|
||||||
|
price_object = data.product_info["price"]
|
||||||
|
self.assertEqual(price_object.get("discount_percent"), 25)
|
||||||
|
self.assertEqual(price_object.get("price_list_rate"), 750)
|
||||||
|
self.assertEqual(price_object.get("formatted_mrp"), "₹ 1,000.00")
|
||||||
|
self.assertEqual(price_object.get("formatted_price"), "₹ 750.00")
|
||||||
|
self.assertEqual(price_object.get("formatted_discount_percent"), "25%")
|
||||||
|
|
||||||
|
# switch to admin and disable show price
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
setup_e_commerce_settings({"show_price": 0})
|
||||||
|
|
||||||
|
# price should not be fetched for logged in user.
|
||||||
|
frappe.set_user("test_contact_customer@example.com")
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||||
|
self.assertFalse(bool(data.product_info["price"]))
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
|
def test_website_item_price_for_guest_user(self):
|
||||||
|
"Check if price details are fetched correctly for guest user."
|
||||||
|
item_code = "Test Mobile Phone"
|
||||||
|
|
||||||
|
# show price for guest user in e commerce settings
|
||||||
|
setup_e_commerce_settings({
|
||||||
|
"show_price": 1,
|
||||||
|
"hide_price_for_guest": 0
|
||||||
|
})
|
||||||
|
|
||||||
|
# price and pricing rule added via setUp
|
||||||
|
|
||||||
|
# switch to guest user
|
||||||
|
frappe.set_user("Guest")
|
||||||
|
|
||||||
|
# price should be fetched
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||||
|
self.assertTrue(bool(data.product_info["price"]))
|
||||||
|
|
||||||
|
price_object = data.product_info["price"]
|
||||||
|
self.assertEqual(price_object.get("discount_percent"), 10)
|
||||||
|
self.assertEqual(price_object.get("price_list_rate"), 900)
|
||||||
|
|
||||||
|
# hide price for guest user
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
setup_e_commerce_settings({"hide_price_for_guest": 1})
|
||||||
|
frappe.set_user("Guest")
|
||||||
|
|
||||||
|
# price should not be fetched
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||||
|
self.assertFalse(bool(data.product_info["price"]))
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
|
def test_website_item_stock_when_out_of_stock(self):
|
||||||
|
"""
|
||||||
|
Check if stock details are fetched correctly for empty inventory when:
|
||||||
|
1) Showing stock availability enabled:
|
||||||
|
- Warehouse unset
|
||||||
|
- Warehouse set
|
||||||
|
2) Showing stock availability disabled
|
||||||
|
"""
|
||||||
|
item_code = "Test Mobile Phone"
|
||||||
|
create_regular_web_item()
|
||||||
|
setup_e_commerce_settings({"show_stock_availability": 1})
|
||||||
|
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||||
|
|
||||||
|
# check if stock details are fetched and item not in stock without warehouse set
|
||||||
|
self.assertFalse(bool(data.product_info["in_stock"]))
|
||||||
|
self.assertFalse(bool(data.product_info["stock_qty"]))
|
||||||
|
|
||||||
|
# set warehouse
|
||||||
|
frappe.db.set_value("Website Item", {"item_code": item_code}, "website_warehouse", "_Test Warehouse - _TC")
|
||||||
|
|
||||||
|
# check if stock details are fetched and item not in stock with warehouse set
|
||||||
|
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||||
|
self.assertFalse(bool(data.product_info["in_stock"]))
|
||||||
|
self.assertEqual(data.product_info["stock_qty"][0][0], 0)
|
||||||
|
|
||||||
|
# disable show stock availability
|
||||||
|
setup_e_commerce_settings({"show_stock_availability": 0})
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||||
|
|
||||||
|
# check if stock detail attributes are not fetched if stock availability is hidden
|
||||||
|
self.assertIsNone(data.product_info.get("in_stock"))
|
||||||
|
self.assertIsNone(data.product_info.get("stock_qty"))
|
||||||
|
self.assertIsNone(data.product_info.get("show_stock_qty"))
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
frappe.get_cached_doc("Website Item", {"item_code": "Test Mobile Phone"}).delete()
|
||||||
|
|
||||||
|
def test_website_item_stock_when_in_stock(self):
|
||||||
|
"""
|
||||||
|
Check if stock details are fetched correctly for available inventory when:
|
||||||
|
1) Showing stock availability enabled:
|
||||||
|
- Warehouse set
|
||||||
|
- Warehouse unset
|
||||||
|
2) Showing stock availability disabled
|
||||||
|
"""
|
||||||
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
|
|
||||||
|
item_code = "Test Mobile Phone"
|
||||||
|
create_regular_web_item()
|
||||||
|
setup_e_commerce_settings({"show_stock_availability": 1})
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
|
||||||
|
# set warehouse
|
||||||
|
frappe.db.set_value("Website Item", {"item_code": item_code}, "website_warehouse", "_Test Warehouse - _TC")
|
||||||
|
|
||||||
|
# stock up item
|
||||||
|
stock_entry = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=2, rate=100)
|
||||||
|
|
||||||
|
# check if stock details are fetched and item is in stock with warehouse set
|
||||||
|
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||||
|
self.assertTrue(bool(data.product_info["in_stock"]))
|
||||||
|
self.assertEqual(data.product_info["stock_qty"][0][0], 2)
|
||||||
|
|
||||||
|
# unset warehouse
|
||||||
|
frappe.db.set_value("Website Item", {"item_code": item_code}, "website_warehouse", "")
|
||||||
|
|
||||||
|
# check if stock details are fetched and item not in stock without warehouse set
|
||||||
|
# (even though it has stock in some warehouse)
|
||||||
|
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||||
|
self.assertFalse(bool(data.product_info["in_stock"]))
|
||||||
|
self.assertFalse(bool(data.product_info["stock_qty"]))
|
||||||
|
|
||||||
|
# disable show stock availability
|
||||||
|
setup_e_commerce_settings({"show_stock_availability": 0})
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||||
|
|
||||||
|
# check if stock detail attributes are not fetched if stock availability is hidden
|
||||||
|
self.assertIsNone(data.product_info.get("in_stock"))
|
||||||
|
self.assertIsNone(data.product_info.get("stock_qty"))
|
||||||
|
self.assertIsNone(data.product_info.get("show_stock_qty"))
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
stock_entry.cancel()
|
||||||
|
frappe.get_cached_doc("Website Item", {"item_code": "Test Mobile Phone"}).delete()
|
||||||
|
|
||||||
|
def test_recommended_item(self):
|
||||||
|
"Check if added recommended items are fetched correctly."
|
||||||
|
item_code = "Test Mobile Phone"
|
||||||
|
web_item = create_regular_web_item(item_code)
|
||||||
|
|
||||||
|
setup_e_commerce_settings({
|
||||||
|
"enable_recommendations": 1,
|
||||||
|
"show_price": 1
|
||||||
|
})
|
||||||
|
|
||||||
|
# create recommended web item and price for it
|
||||||
|
recommended_web_item = create_regular_web_item("Test Mobile Phone 1")
|
||||||
|
make_web_item_price(item_code="Test Mobile Phone 1")
|
||||||
|
|
||||||
|
# add recommended item to first web item
|
||||||
|
web_item.append("recommended_items", {"website_item": recommended_web_item.name})
|
||||||
|
web_item.save()
|
||||||
|
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
e_commerce_settings = get_shopping_cart_settings()
|
||||||
|
recommended_items = web_item.get_recommended_items(e_commerce_settings)
|
||||||
|
|
||||||
|
# test results if show price is enabled
|
||||||
|
self.assertEqual(len(recommended_items), 1)
|
||||||
|
recomm_item = recommended_items[0]
|
||||||
|
self.assertEqual(recomm_item.get("website_item_name"), "Test Mobile Phone 1")
|
||||||
|
self.assertTrue(bool(recomm_item.get("price_info"))) # price fetched
|
||||||
|
|
||||||
|
price_info = recomm_item.get("price_info")
|
||||||
|
self.assertEqual(price_info.get("price_list_rate"), 1000)
|
||||||
|
self.assertEqual(price_info.get("formatted_price"), "₹ 1,000.00")
|
||||||
|
|
||||||
|
# test results if show price is disabled
|
||||||
|
setup_e_commerce_settings({"show_price": 0})
|
||||||
|
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
e_commerce_settings = get_shopping_cart_settings()
|
||||||
|
recommended_items = web_item.get_recommended_items(e_commerce_settings)
|
||||||
|
|
||||||
|
self.assertEqual(len(recommended_items), 1)
|
||||||
|
self.assertFalse(bool(recommended_items[0].get("price_info"))) # price not fetched
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
web_item.delete()
|
||||||
|
recommended_web_item.delete()
|
||||||
|
frappe.get_cached_doc("Item", "Test Mobile Phone 1").delete()
|
||||||
|
|
||||||
|
def test_recommended_item_for_guest_user(self):
|
||||||
|
"Check if added recommended items are fetched correctly for guest user."
|
||||||
|
item_code = "Test Mobile Phone"
|
||||||
|
web_item = create_regular_web_item(item_code)
|
||||||
|
|
||||||
|
# price visible to guests
|
||||||
|
setup_e_commerce_settings({
|
||||||
|
"enable_recommendations": 1,
|
||||||
|
"show_price": 1,
|
||||||
|
"hide_price_for_guest": 0
|
||||||
|
})
|
||||||
|
|
||||||
|
# create recommended web item and price for it
|
||||||
|
recommended_web_item = create_regular_web_item("Test Mobile Phone 1")
|
||||||
|
make_web_item_price(item_code="Test Mobile Phone 1")
|
||||||
|
|
||||||
|
# add recommended item to first web item
|
||||||
|
web_item.append("recommended_items", {"website_item": recommended_web_item.name})
|
||||||
|
web_item.save()
|
||||||
|
|
||||||
|
frappe.set_user("Guest")
|
||||||
|
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
e_commerce_settings = get_shopping_cart_settings()
|
||||||
|
recommended_items = web_item.get_recommended_items(e_commerce_settings)
|
||||||
|
|
||||||
|
# test results if show price is enabled
|
||||||
|
self.assertEqual(len(recommended_items), 1)
|
||||||
|
self.assertTrue(bool(recommended_items[0].get("price_info"))) # price fetched
|
||||||
|
|
||||||
|
# price hidden from guests
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
setup_e_commerce_settings({"hide_price_for_guest": 1})
|
||||||
|
frappe.set_user("Guest")
|
||||||
|
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
e_commerce_settings = get_shopping_cart_settings()
|
||||||
|
recommended_items = web_item.get_recommended_items(e_commerce_settings)
|
||||||
|
|
||||||
|
# test results if show price is enabled
|
||||||
|
self.assertEqual(len(recommended_items), 1)
|
||||||
|
self.assertFalse(bool(recommended_items[0].get("price_info"))) # price fetched
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
web_item.delete()
|
||||||
|
recommended_web_item.delete()
|
||||||
|
frappe.get_cached_doc("Item", "Test Mobile Phone 1").delete()
|
||||||
|
|
||||||
|
def create_regular_web_item(item_code=None, item_args=None, web_args=None):
|
||||||
|
"Create Regular Item and Website Item."
|
||||||
|
item_code = item_code or "Test Mobile Phone"
|
||||||
|
item = make_item(item_code, properties=item_args)
|
||||||
|
|
||||||
|
if not frappe.db.exists("Website Item", {"item_code": item_code}):
|
||||||
|
web_item = make_website_item(item, save=False)
|
||||||
|
if web_args:
|
||||||
|
web_item.update(web_args)
|
||||||
|
web_item.save()
|
||||||
|
else:
|
||||||
|
web_item = frappe.get_cached_doc("Website Item", {"item_code": item_code})
|
||||||
|
|
||||||
|
return web_item
|
||||||
|
|
||||||
|
def make_web_item_price(**kwargs):
|
||||||
|
item_code = kwargs.get("item_code")
|
||||||
|
if not item_code:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not frappe.db.exists("Item Price", {"item_code": item_code}):
|
||||||
|
item_price = frappe.get_doc({
|
||||||
|
"doctype": "Item Price",
|
||||||
|
"item_code": item_code,
|
||||||
|
"price_list": kwargs.get("price_list") or "_Test Price List India",
|
||||||
|
"price_list_rate": kwargs.get("price_list_rate") or 1000
|
||||||
|
})
|
||||||
|
item_price.insert()
|
||||||
|
else:
|
||||||
|
item_price = frappe.get_cached_doc("Item Price", {"item_code": item_code})
|
||||||
|
|
||||||
|
return item_price
|
||||||
|
|
||||||
|
def make_web_pricing_rule(**kwargs):
|
||||||
|
title = kwargs.get("title")
|
||||||
|
if not title:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not frappe.db.exists("Pricing Rule", title):
|
||||||
|
pricing_rule = frappe.get_doc({
|
||||||
|
"doctype": "Pricing Rule",
|
||||||
|
"title": title,
|
||||||
|
"apply_on": kwargs.get("apply_on") or "Item Code",
|
||||||
|
"items": [{
|
||||||
|
"item_code": kwargs.get("item_code")
|
||||||
|
}],
|
||||||
|
"selling": kwargs.get("selling") or 0,
|
||||||
|
"buying": kwargs.get("buying") or 0,
|
||||||
|
"rate_or_discount": kwargs.get("rate_or_discount") or "Discount Percentage",
|
||||||
|
"discount_percentage": kwargs.get("discount_percentage") or 10,
|
||||||
|
"company": kwargs.get("company") or "_Test Company",
|
||||||
|
"currency": kwargs.get("currency") or "INR",
|
||||||
|
"for_price_list": kwargs.get("price_list") or "_Test Price List India",
|
||||||
|
"applicable_for": kwargs.get("applicable_for") or "",
|
||||||
|
"customer": kwargs.get("customer") or "",
|
||||||
|
})
|
||||||
|
pricing_rule.insert()
|
||||||
|
else:
|
||||||
|
pricing_rule = frappe.get_doc("Pricing Rule", {"title": title})
|
||||||
|
|
||||||
|
return pricing_rule
|
||||||
|
|
||||||
|
|
||||||
|
def create_user_and_customer_if_not_exists(email, first_name = None):
|
||||||
|
if frappe.db.exists("User", email):
|
||||||
|
return
|
||||||
|
|
||||||
|
frappe.get_doc({
|
||||||
|
"doctype": "User",
|
||||||
|
"user_type": "Website User",
|
||||||
|
"email": email,
|
||||||
|
"send_welcome_email": 0,
|
||||||
|
"first_name": first_name or email.split("@")[0]
|
||||||
|
}).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
contact = frappe.get_last_doc("Contact", filters={"email_id": email})
|
||||||
|
link = contact.append('links', {})
|
||||||
|
link.link_doctype = "Customer"
|
||||||
|
link.link_name = "_Test Customer"
|
||||||
|
link.link_title = "_Test Customer"
|
||||||
|
contact.save()
|
||||||
|
|
||||||
|
test_dependencies = ["Price List", "Item Price", "Customer", "Contact", "Item"]
|
24
erpnext/e_commerce/doctype/website_item/website_item.js
Normal file
24
erpnext/e_commerce/doctype/website_item/website_item.js
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
415
erpnext/e_commerce/doctype/website_item/website_item.json
Normal file
415
erpnext/e_commerce/doctype/website_item/website_item.json
Normal 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
|
||||||
|
}
|
441
erpnext/e_commerce/doctype/website_item/website_item.py
Normal file
441
erpnext/e_commerce/doctype/website_item/website_item.py
Normal file
@ -0,0 +1,441 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import cint, cstr, flt, random_string
|
||||||
|
from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow
|
||||||
|
from frappe.website.website_generator import WebsiteGenerator
|
||||||
|
|
||||||
|
from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
|
||||||
|
from erpnext.e_commerce.redisearch_utils import (
|
||||||
|
delete_item_from_index,
|
||||||
|
insert_item_to_index,
|
||||||
|
update_index_for_item,
|
||||||
|
)
|
||||||
|
from erpnext.e_commerce.shopping_cart.cart import _set_price_list
|
||||||
|
from erpnext.setup.doctype.item_group.item_group import (
|
||||||
|
get_parent_item_groups,
|
||||||
|
invalidate_cache_for,
|
||||||
|
)
|
||||||
|
from erpnext.utilities.product import get_price
|
||||||
|
|
||||||
|
|
||||||
|
class WebsiteItem(WebsiteGenerator):
|
||||||
|
website = frappe._dict(
|
||||||
|
page_title_field="web_item_name",
|
||||||
|
condition_field="published",
|
||||||
|
template="templates/generators/item/item.html",
|
||||||
|
no_cache=1
|
||||||
|
)
|
||||||
|
|
||||||
|
def autoname(self):
|
||||||
|
# use naming series to accomodate items with same name (different item code)
|
||||||
|
from frappe.model.naming import make_autoname
|
||||||
|
|
||||||
|
from erpnext.setup.doctype.naming_series.naming_series import get_default_naming_series
|
||||||
|
|
||||||
|
naming_series = get_default_naming_series("Website Item")
|
||||||
|
if not self.name and naming_series:
|
||||||
|
self.name = make_autoname(naming_series, doc=self)
|
||||||
|
|
||||||
|
def onload(self):
|
||||||
|
super(WebsiteItem, self).onload()
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
super(WebsiteItem, self).validate()
|
||||||
|
|
||||||
|
if not self.item_code:
|
||||||
|
frappe.throw(_("Item Code is required"), title=_("Mandatory"))
|
||||||
|
|
||||||
|
self.validate_duplicate_website_item()
|
||||||
|
self.validate_website_image()
|
||||||
|
self.make_thumbnail()
|
||||||
|
self.publish_unpublish_desk_item(publish=True)
|
||||||
|
|
||||||
|
if not self.get("__islocal"):
|
||||||
|
wig = frappe.qb.DocType("Website Item Group")
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(wig)
|
||||||
|
.select(wig.item_group)
|
||||||
|
.where(
|
||||||
|
(wig.parentfield == "website_item_groups")
|
||||||
|
& (wig.parenttype == "Website Item")
|
||||||
|
& (wig.parent == self.name)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result = query.run(as_list=True)
|
||||||
|
|
||||||
|
self.old_website_item_groups = [x[0] for x in result]
|
||||||
|
|
||||||
|
def on_update(self):
|
||||||
|
invalidate_cache_for_web_item(self)
|
||||||
|
self.update_template_item()
|
||||||
|
|
||||||
|
def on_trash(self):
|
||||||
|
super(WebsiteItem, self).on_trash()
|
||||||
|
delete_item_from_index(self)
|
||||||
|
self.publish_unpublish_desk_item(publish=False)
|
||||||
|
|
||||||
|
def validate_duplicate_website_item(self):
|
||||||
|
existing_web_item = frappe.db.exists("Website Item", {"item_code": self.item_code})
|
||||||
|
if existing_web_item and existing_web_item != self.name:
|
||||||
|
message = _("Website Item already exists against Item {0}").format(frappe.bold(self.item_code))
|
||||||
|
frappe.throw(message, title=_("Already Published"))
|
||||||
|
|
||||||
|
def publish_unpublish_desk_item(self, publish=True):
|
||||||
|
if frappe.db.get_value("Item", self.item_code, "published_in_website") and publish:
|
||||||
|
return # if already published don't publish again
|
||||||
|
frappe.db.set_value("Item", self.item_code, "published_in_website", publish)
|
||||||
|
|
||||||
|
def make_route(self):
|
||||||
|
"""Called from set_route in WebsiteGenerator."""
|
||||||
|
if not self.route:
|
||||||
|
return cstr(frappe.db.get_value('Item Group', self.item_group,
|
||||||
|
'route')) + '/' + self.scrub((self.item_name if self.item_name else self.item_code) + '-' + random_string(5))
|
||||||
|
|
||||||
|
def update_template_item(self):
|
||||||
|
"""Publish Template Item if Variant is published."""
|
||||||
|
if self.variant_of:
|
||||||
|
if self.published:
|
||||||
|
# show template
|
||||||
|
template_item = frappe.get_doc("Item", self.variant_of)
|
||||||
|
|
||||||
|
if not template_item.published_in_website:
|
||||||
|
template_item.flags.ignore_permissions = True
|
||||||
|
make_website_item(template_item)
|
||||||
|
|
||||||
|
def validate_website_image(self):
|
||||||
|
if frappe.flags.in_import:
|
||||||
|
return
|
||||||
|
|
||||||
|
"""Validate if the website image is a public file"""
|
||||||
|
auto_set_website_image = False
|
||||||
|
if not self.website_image and self.image:
|
||||||
|
auto_set_website_image = True
|
||||||
|
self.website_image = self.image
|
||||||
|
|
||||||
|
if not self.website_image:
|
||||||
|
return
|
||||||
|
|
||||||
|
# find if website image url exists as public
|
||||||
|
file_doc = frappe.get_all(
|
||||||
|
"File",
|
||||||
|
filters={
|
||||||
|
"file_url": self.website_image
|
||||||
|
},
|
||||||
|
fields=["name", "is_private"],
|
||||||
|
order_by="is_private asc",
|
||||||
|
limit_page_length=1
|
||||||
|
)
|
||||||
|
|
||||||
|
if file_doc:
|
||||||
|
file_doc = file_doc[0]
|
||||||
|
|
||||||
|
if not file_doc:
|
||||||
|
if not auto_set_website_image:
|
||||||
|
frappe.msgprint(_("Website Image {0} attached to Item {1} cannot be found").format(self.website_image, self.name))
|
||||||
|
|
||||||
|
self.website_image = None
|
||||||
|
|
||||||
|
elif file_doc.is_private:
|
||||||
|
if not auto_set_website_image:
|
||||||
|
frappe.msgprint(_("Website Image should be a public file or website URL"))
|
||||||
|
|
||||||
|
self.website_image = None
|
||||||
|
|
||||||
|
def make_thumbnail(self):
|
||||||
|
"""Make a thumbnail of `website_image`"""
|
||||||
|
if frappe.flags.in_import or frappe.flags.in_migrate:
|
||||||
|
return
|
||||||
|
|
||||||
|
import requests.exceptions
|
||||||
|
|
||||||
|
if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"):
|
||||||
|
self.thumbnail = None
|
||||||
|
|
||||||
|
if self.website_image and not self.thumbnail:
|
||||||
|
file_doc = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
file_doc = frappe.get_doc("File", {
|
||||||
|
"file_url": self.website_image,
|
||||||
|
"attached_to_doctype": "Website Item",
|
||||||
|
"attached_to_name": self.name
|
||||||
|
})
|
||||||
|
except frappe.DoesNotExistError:
|
||||||
|
pass
|
||||||
|
# cleanup
|
||||||
|
frappe.local.message_log.pop()
|
||||||
|
|
||||||
|
except requests.exceptions.HTTPError:
|
||||||
|
frappe.msgprint(_("Warning: Invalid attachment {0}").format(self.website_image))
|
||||||
|
self.website_image = None
|
||||||
|
|
||||||
|
except requests.exceptions.SSLError:
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Warning: Invalid SSL certificate on attachment {0}").format(self.website_image))
|
||||||
|
self.website_image = None
|
||||||
|
|
||||||
|
# for CSV import
|
||||||
|
if self.website_image and not file_doc:
|
||||||
|
try:
|
||||||
|
file_doc = frappe.get_doc({
|
||||||
|
"doctype": "File",
|
||||||
|
"file_url": self.website_image,
|
||||||
|
"attached_to_doctype": "Website Item",
|
||||||
|
"attached_to_name": self.name
|
||||||
|
}).save()
|
||||||
|
|
||||||
|
except IOError:
|
||||||
|
self.website_image = None
|
||||||
|
|
||||||
|
if file_doc:
|
||||||
|
if not file_doc.thumbnail_url:
|
||||||
|
file_doc.make_thumbnail()
|
||||||
|
|
||||||
|
self.thumbnail = file_doc.thumbnail_url
|
||||||
|
|
||||||
|
def get_context(self, context):
|
||||||
|
context.show_search = True
|
||||||
|
context.search_link = "/search"
|
||||||
|
context.body_class = "product-page"
|
||||||
|
|
||||||
|
context.parents = get_parent_item_groups(self.item_group, from_item=True) # breadcumbs
|
||||||
|
self.attributes = frappe.get_all(
|
||||||
|
"Item Variant Attribute",
|
||||||
|
fields=["attribute", "attribute_value"],
|
||||||
|
filters={"parent": self.item_code}
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.slideshow:
|
||||||
|
context.update(get_slideshow(self))
|
||||||
|
|
||||||
|
self.set_metatags(context)
|
||||||
|
self.set_shopping_cart_data(context)
|
||||||
|
|
||||||
|
settings = context.shopping_cart.cart_settings
|
||||||
|
|
||||||
|
self.get_product_details_section(context)
|
||||||
|
|
||||||
|
if settings.get("enable_reviews"):
|
||||||
|
reviews_data = get_item_reviews(self.name)
|
||||||
|
context.update(reviews_data)
|
||||||
|
context.reviews = context.reviews[:4]
|
||||||
|
|
||||||
|
context.wished = False
|
||||||
|
if frappe.db.exists("Wishlist Item", {"item_code": self.item_code, "parent": frappe.session.user}):
|
||||||
|
context.wished = True
|
||||||
|
|
||||||
|
context.user_is_customer = check_if_user_is_customer()
|
||||||
|
|
||||||
|
context.recommended_items = None
|
||||||
|
if settings and settings.enable_recommendations:
|
||||||
|
context.recommended_items = self.get_recommended_items(settings)
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def set_selected_attributes(self, variants, context, attribute_values_available):
|
||||||
|
for variant in variants:
|
||||||
|
variant.attributes = frappe.get_all(
|
||||||
|
"Item Variant Attribute",
|
||||||
|
filters={"parent": variant.name},
|
||||||
|
fields=["attribute", "attribute_value as value"])
|
||||||
|
|
||||||
|
# make an attribute-value map for easier access in templates
|
||||||
|
variant.attribute_map = frappe._dict(
|
||||||
|
{attr.attribute : attr.value for attr in variant.attributes}
|
||||||
|
)
|
||||||
|
|
||||||
|
for attr in variant.attributes:
|
||||||
|
values = attribute_values_available.setdefault(attr.attribute, [])
|
||||||
|
if attr.value not in values:
|
||||||
|
values.append(attr.value)
|
||||||
|
|
||||||
|
if variant.name == context.variant.name:
|
||||||
|
context.selected_attributes[attr.attribute] = attr.value
|
||||||
|
|
||||||
|
def set_attribute_values(self, attributes, context, attribute_values_available):
|
||||||
|
for attr in attributes:
|
||||||
|
values = context.attribute_values.setdefault(attr.attribute, [])
|
||||||
|
|
||||||
|
if cint(frappe.db.get_value("Item Attribute", attr.attribute, "numeric_values")):
|
||||||
|
for val in sorted(attribute_values_available.get(attr.attribute, []), key=flt):
|
||||||
|
values.append(val)
|
||||||
|
else:
|
||||||
|
# get list of values defined (for sequence)
|
||||||
|
for attr_value in frappe.db.get_all("Item Attribute Value",
|
||||||
|
fields=["attribute_value"],
|
||||||
|
filters={"parent": attr.attribute}, order_by="idx asc"):
|
||||||
|
|
||||||
|
if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []):
|
||||||
|
values.append(attr_value.attribute_value)
|
||||||
|
|
||||||
|
def set_metatags(self, context):
|
||||||
|
context.metatags = frappe._dict({})
|
||||||
|
|
||||||
|
safe_description = frappe.utils.to_markdown(self.description)
|
||||||
|
|
||||||
|
context.metatags.url = frappe.utils.get_url() + '/' + context.route
|
||||||
|
|
||||||
|
if context.website_image:
|
||||||
|
if context.website_image.startswith('http'):
|
||||||
|
url = context.website_image
|
||||||
|
else:
|
||||||
|
url = frappe.utils.get_url() + context.website_image
|
||||||
|
context.metatags.image = url
|
||||||
|
|
||||||
|
context.metatags.description = safe_description[:300]
|
||||||
|
|
||||||
|
context.metatags.title = self.web_item_name or self.item_name or self.item_code
|
||||||
|
|
||||||
|
context.metatags['og:type'] = 'product'
|
||||||
|
context.metatags['og:site_name'] = 'ERPNext'
|
||||||
|
|
||||||
|
def set_shopping_cart_data(self, context):
|
||||||
|
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
|
||||||
|
context.shopping_cart = get_product_info_for_website(self.item_code, skip_quotation_creation=True)
|
||||||
|
|
||||||
|
def copy_specification_from_item_group(self):
|
||||||
|
self.set("website_specifications", [])
|
||||||
|
if self.item_group:
|
||||||
|
for label, desc in frappe.db.get_values("Item Website Specification",
|
||||||
|
{"parent": self.item_group}, ["label", "description"]):
|
||||||
|
row = self.append("website_specifications")
|
||||||
|
row.label = label
|
||||||
|
row.description = desc
|
||||||
|
|
||||||
|
def get_product_details_section(self, context):
|
||||||
|
""" Get section with tabs or website specifications. """
|
||||||
|
context.show_tabs = self.show_tabbed_section
|
||||||
|
if self.show_tabbed_section and (self.tabs or self.website_specifications):
|
||||||
|
context.tabs = self.get_tabs()
|
||||||
|
else:
|
||||||
|
context.website_specifications = self.website_specifications
|
||||||
|
|
||||||
|
def get_tabs(self):
|
||||||
|
tab_values = {}
|
||||||
|
tab_values["tab_1_title"] = "Product Details"
|
||||||
|
tab_values["tab_1_content"] = frappe.render_template(
|
||||||
|
"templates/generators/item/item_specifications.html",
|
||||||
|
{
|
||||||
|
"website_specifications": self.website_specifications,
|
||||||
|
"show_tabs": self.show_tabbed_section
|
||||||
|
})
|
||||||
|
|
||||||
|
for row in self.tabs:
|
||||||
|
tab_values[f"tab_{row.idx + 1}_title"] = _(row.label)
|
||||||
|
tab_values[f"tab_{row.idx + 1}_content"] = row.content
|
||||||
|
|
||||||
|
return tab_values
|
||||||
|
|
||||||
|
def get_recommended_items(self, settings):
|
||||||
|
ri = frappe.qb.DocType("Recommended Items")
|
||||||
|
wi = frappe.qb.DocType("Website Item")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(ri)
|
||||||
|
.join(wi).on(ri.item_code == wi.item_code)
|
||||||
|
.select(
|
||||||
|
ri.item_code, ri.route,
|
||||||
|
ri.website_item_name,
|
||||||
|
ri.website_item_thumbnail
|
||||||
|
).where(
|
||||||
|
(ri.parent == self.name)
|
||||||
|
& (wi.published == 1)
|
||||||
|
).orderby(ri.idx)
|
||||||
|
)
|
||||||
|
items = query.run(as_dict=True)
|
||||||
|
|
||||||
|
if settings.show_price:
|
||||||
|
is_guest = frappe.session.user == "Guest"
|
||||||
|
# Show Price if logged in.
|
||||||
|
# If not logged in and price is hidden for guest, skip price fetch.
|
||||||
|
if is_guest and settings.hide_price_for_guest:
|
||||||
|
return items
|
||||||
|
|
||||||
|
selling_price_list = _set_price_list(settings, None)
|
||||||
|
for item in items:
|
||||||
|
item.price_info = get_price(
|
||||||
|
item.item_code,
|
||||||
|
selling_price_list,
|
||||||
|
settings.default_customer_group,
|
||||||
|
settings.company
|
||||||
|
)
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
def invalidate_cache_for_web_item(doc):
|
||||||
|
"""Invalidate Website Item Group cache and rebuild ItemVariantsCacheManager."""
|
||||||
|
from erpnext.stock.doctype.item.item import invalidate_item_variants_cache_for_website
|
||||||
|
|
||||||
|
invalidate_cache_for(doc, doc.item_group)
|
||||||
|
|
||||||
|
website_item_groups = list(set((doc.get("old_website_item_groups") or [])
|
||||||
|
+ [d.item_group for d in doc.get({"doctype": "Website Item Group"}) if d.item_group]))
|
||||||
|
|
||||||
|
for item_group in website_item_groups:
|
||||||
|
invalidate_cache_for(doc, item_group)
|
||||||
|
|
||||||
|
# Update Search Cache
|
||||||
|
update_index_for_item(doc)
|
||||||
|
|
||||||
|
invalidate_item_variants_cache_for_website(doc)
|
||||||
|
|
||||||
|
def on_doctype_update():
|
||||||
|
# since route is a Text column, it needs a length for indexing
|
||||||
|
frappe.db.add_index("Website Item", ["route(500)"])
|
||||||
|
|
||||||
|
frappe.db.add_index("Website Item", ["item_group"])
|
||||||
|
frappe.db.add_index("Website Item", ["brand"])
|
||||||
|
|
||||||
|
def check_if_user_is_customer(user=None):
|
||||||
|
from frappe.contacts.doctype.contact.contact import get_contact_name
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
user = frappe.session.user
|
||||||
|
|
||||||
|
contact_name = get_contact_name(user)
|
||||||
|
customer = None
|
||||||
|
|
||||||
|
if contact_name:
|
||||||
|
contact = frappe.get_doc('Contact', contact_name)
|
||||||
|
for link in contact.links:
|
||||||
|
if link.link_doctype == "Customer":
|
||||||
|
customer = link.link_name
|
||||||
|
break
|
||||||
|
|
||||||
|
return True if customer else False
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def make_website_item(doc, save=True):
|
||||||
|
if not doc:
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(doc, str):
|
||||||
|
doc = json.loads(doc)
|
||||||
|
|
||||||
|
if frappe.db.exists("Website Item", {"item_code": doc.get("item_code")}):
|
||||||
|
message = _("Website Item already exists against {0}").format(frappe.bold(doc.get("item_code")))
|
||||||
|
frappe.throw(message, title=_("Already Published"))
|
||||||
|
|
||||||
|
website_item = frappe.new_doc("Website Item")
|
||||||
|
website_item.web_item_name = doc.get("item_name")
|
||||||
|
|
||||||
|
fields_to_map = ["item_code", "item_name", "item_group", "stock_uom", "brand", "image",
|
||||||
|
"has_variants", "variant_of", "description"]
|
||||||
|
for field in fields_to_map:
|
||||||
|
website_item.update({field: doc.get(field)})
|
||||||
|
|
||||||
|
if not save:
|
||||||
|
return website_item
|
||||||
|
|
||||||
|
website_item.save()
|
||||||
|
|
||||||
|
# Add to search cache
|
||||||
|
insert_item_to_index(website_item)
|
||||||
|
|
||||||
|
return [website_item.name, website_item.web_item_name]
|
20
erpnext/e_commerce/doctype/website_item/website_item_list.js
Normal file
20
erpnext/e_commerce/doctype/website_item/website_item_list.js
Normal 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"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -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
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class WebsiteItemTabbedSection(Document):
|
||||||
|
pass
|
43
erpnext/e_commerce/doctype/website_offer/website_offer.json
Normal file
43
erpnext/e_commerce/doctype/website_offer/website_offer.json
Normal 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
|
||||||
|
}
|
14
erpnext/e_commerce/doctype/website_offer/website_offer.py
Normal file
14
erpnext/e_commerce/doctype/website_offer/website_offer.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class WebsiteOffer(Document):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def get_offer_details(offer_id):
|
||||||
|
return frappe.db.get_value('Website Offer', {'name': offer_id}, ['offer_details'])
|
0
erpnext/e_commerce/doctype/wishlist/__init__.py
Normal file
0
erpnext/e_commerce/doctype/wishlist/__init__.py
Normal file
102
erpnext/e_commerce/doctype/wishlist/test_wishlist.py
Normal file
102
erpnext/e_commerce/doctype/wishlist/test_wishlist.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
||||||
|
|
||||||
|
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||||
|
from erpnext.e_commerce.doctype.wishlist.wishlist import add_to_wishlist, remove_from_wishlist
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
|
|
||||||
|
class TestWishlist(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
item = make_item("Test Phone Series X")
|
||||||
|
if not frappe.db.exists("Website Item", {"item_code": "Test Phone Series X"}):
|
||||||
|
make_website_item(item, save=True)
|
||||||
|
|
||||||
|
item = make_item("Test Phone Series Y")
|
||||||
|
if not frappe.db.exists("Website Item", {"item_code": "Test Phone Series Y"}):
|
||||||
|
make_website_item(item, save=True)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.get_cached_doc("Website Item", {"item_code": "Test Phone Series X"}).delete()
|
||||||
|
frappe.get_cached_doc("Website Item", {"item_code": "Test Phone Series Y"}).delete()
|
||||||
|
frappe.get_cached_doc("Item", "Test Phone Series X").delete()
|
||||||
|
frappe.get_cached_doc("Item", "Test Phone Series Y").delete()
|
||||||
|
|
||||||
|
def test_add_remove_items_in_wishlist(self):
|
||||||
|
"Check if items are added and removed from user's wishlist."
|
||||||
|
# add first item
|
||||||
|
add_to_wishlist("Test Phone Series X")
|
||||||
|
|
||||||
|
# check if wishlist was created and item was added
|
||||||
|
self.assertTrue(frappe.db.exists("Wishlist", {"user": frappe.session.user}))
|
||||||
|
self.assertTrue(frappe.db.exists("Wishlist Item", {"item_code": "Test Phone Series X", "parent": frappe.session.user}))
|
||||||
|
|
||||||
|
# add second item to wishlist
|
||||||
|
add_to_wishlist("Test Phone Series Y")
|
||||||
|
wishlist_length = frappe.db.get_value(
|
||||||
|
"Wishlist Item",
|
||||||
|
{"parent": frappe.session.user},
|
||||||
|
"count(*)"
|
||||||
|
)
|
||||||
|
self.assertEqual(wishlist_length, 2)
|
||||||
|
|
||||||
|
remove_from_wishlist("Test Phone Series X")
|
||||||
|
remove_from_wishlist("Test Phone Series Y")
|
||||||
|
|
||||||
|
wishlist_length = frappe.db.get_value(
|
||||||
|
"Wishlist Item",
|
||||||
|
{"parent": frappe.session.user},
|
||||||
|
"count(*)"
|
||||||
|
)
|
||||||
|
self.assertIsNone(frappe.db.exists("Wishlist Item", {"parent": frappe.session.user}))
|
||||||
|
self.assertEqual(wishlist_length, 0)
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
frappe.get_doc("Wishlist", {"user": frappe.session.user}).delete()
|
||||||
|
|
||||||
|
def test_add_remove_in_wishlist_multiple_users(self):
|
||||||
|
"Check if items are added and removed from the correct user's wishlist."
|
||||||
|
test_user = create_user("test_reviewer@example.com", "Customer")
|
||||||
|
test_user_1 = create_user("test_reviewer_1@example.com", "Customer")
|
||||||
|
|
||||||
|
# add to wishlist for first user
|
||||||
|
frappe.set_user(test_user.name)
|
||||||
|
add_to_wishlist("Test Phone Series X")
|
||||||
|
|
||||||
|
# add to wishlist for second user
|
||||||
|
frappe.set_user(test_user_1.name)
|
||||||
|
add_to_wishlist("Test Phone Series X")
|
||||||
|
|
||||||
|
# check wishlist and its content for users
|
||||||
|
self.assertTrue(frappe.db.exists("Wishlist", {"user": test_user.name}))
|
||||||
|
self.assertTrue(frappe.db.exists("Wishlist Item",
|
||||||
|
{"item_code": "Test Phone Series X", "parent": test_user.name}))
|
||||||
|
|
||||||
|
self.assertTrue(frappe.db.exists("Wishlist", {"user": test_user_1.name}))
|
||||||
|
self.assertTrue(frappe.db.exists("Wishlist Item",
|
||||||
|
{"item_code": "Test Phone Series X", "parent": test_user_1.name}))
|
||||||
|
|
||||||
|
# remove item for second user
|
||||||
|
remove_from_wishlist("Test Phone Series X")
|
||||||
|
|
||||||
|
# make sure item was removed for second user and not first
|
||||||
|
self.assertFalse(frappe.db.exists("Wishlist Item",
|
||||||
|
{"item_code": "Test Phone Series X", "parent": test_user_1.name}))
|
||||||
|
self.assertTrue(frappe.db.exists("Wishlist Item",
|
||||||
|
{"item_code": "Test Phone Series X", "parent": test_user.name}))
|
||||||
|
|
||||||
|
# remove item for first user
|
||||||
|
frappe.set_user(test_user.name)
|
||||||
|
remove_from_wishlist("Test Phone Series X")
|
||||||
|
self.assertFalse(frappe.db.exists("Wishlist Item",
|
||||||
|
{"item_code": "Test Phone Series X", "parent": test_user.name}))
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
frappe.get_doc("Wishlist", {"user": test_user.name}).delete()
|
||||||
|
frappe.get_doc("Wishlist", {"user": test_user_1.name}).delete()
|
8
erpnext/e_commerce/doctype/wishlist/wishlist.js
Normal file
8
erpnext/e_commerce/doctype/wishlist/wishlist.js
Normal 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) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
});
|
65
erpnext/e_commerce/doctype/wishlist/wishlist.json
Normal file
65
erpnext/e_commerce/doctype/wishlist/wishlist.json
Normal 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
|
||||||
|
}
|
68
erpnext/e_commerce/doctype/wishlist/wishlist.py
Normal file
68
erpnext/e_commerce/doctype/wishlist/wishlist.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class Wishlist(Document):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def add_to_wishlist(item_code):
|
||||||
|
"""Insert Item into wishlist."""
|
||||||
|
|
||||||
|
if frappe.db.exists("Wishlist Item", {"item_code": item_code, "parent": frappe.session.user}):
|
||||||
|
return
|
||||||
|
|
||||||
|
web_item_data = frappe.db.get_value(
|
||||||
|
"Website Item",
|
||||||
|
{"item_code": item_code},
|
||||||
|
["image", "website_warehouse", "name", "web_item_name", "item_name", "item_group", "route"],
|
||||||
|
as_dict=1)
|
||||||
|
|
||||||
|
wished_item_dict = {
|
||||||
|
"item_code": item_code,
|
||||||
|
"item_name": web_item_data.get("item_name"),
|
||||||
|
"item_group": web_item_data.get("item_group"),
|
||||||
|
"website_item": web_item_data.get("name"),
|
||||||
|
"web_item_name": web_item_data.get("web_item_name"),
|
||||||
|
"image": web_item_data.get("image"),
|
||||||
|
"warehouse": web_item_data.get("website_warehouse"),
|
||||||
|
"route": web_item_data.get("route")
|
||||||
|
}
|
||||||
|
|
||||||
|
if not frappe.db.exists("Wishlist", frappe.session.user):
|
||||||
|
# initialise wishlist
|
||||||
|
wishlist = frappe.get_doc({"doctype": "Wishlist"})
|
||||||
|
wishlist.user = frappe.session.user
|
||||||
|
wishlist.append("items", wished_item_dict)
|
||||||
|
wishlist.save(ignore_permissions=True)
|
||||||
|
else:
|
||||||
|
wishlist = frappe.get_doc("Wishlist", frappe.session.user)
|
||||||
|
item = wishlist.append('items', wished_item_dict)
|
||||||
|
item.db_insert()
|
||||||
|
|
||||||
|
if hasattr(frappe.local, "cookie_manager"):
|
||||||
|
frappe.local.cookie_manager.set_cookie("wish_count", str(len(wishlist.items)))
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def remove_from_wishlist(item_code):
|
||||||
|
if frappe.db.exists("Wishlist Item", {"item_code": item_code, "parent": frappe.session.user}):
|
||||||
|
frappe.db.delete(
|
||||||
|
"Wishlist Item",
|
||||||
|
{
|
||||||
|
"item_code": item_code,
|
||||||
|
"parent": frappe.session.user
|
||||||
|
}
|
||||||
|
)
|
||||||
|
frappe.db.commit() # nosemgrep
|
||||||
|
|
||||||
|
wishlist_items = frappe.db.get_values(
|
||||||
|
"Wishlist Item",
|
||||||
|
filters={"parent": frappe.session.user}
|
||||||
|
)
|
||||||
|
|
||||||
|
if hasattr(frappe.local, "cookie_manager"):
|
||||||
|
frappe.local.cookie_manager.set_cookie("wish_count", str(len(wishlist_items)))
|
147
erpnext/e_commerce/doctype/wishlist_item/wishlist_item.json
Normal file
147
erpnext/e_commerce/doctype/wishlist_item/wishlist_item.json
Normal 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
|
||||||
|
}
|
10
erpnext/e_commerce/doctype/wishlist_item/wishlist_item.py
Normal file
10
erpnext/e_commerce/doctype/wishlist_item/wishlist_item.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class WishlistItem(Document):
|
||||||
|
pass
|
@ -6,6 +6,7 @@ from whoosh.fields import ID, KEYWORD, TEXT, Schema
|
|||||||
from whoosh.qparser import FieldsPlugin, MultifieldParser, WildcardPlugin
|
from whoosh.qparser import FieldsPlugin, MultifieldParser, WildcardPlugin
|
||||||
from whoosh.query import Prefix
|
from whoosh.query import Prefix
|
||||||
|
|
||||||
|
# TODO: Make obsolete
|
||||||
INDEX_NAME = "products"
|
INDEX_NAME = "products"
|
||||||
|
|
||||||
class ProductSearch(FullTextSearch):
|
class ProductSearch(FullTextSearch):
|
||||||
@ -111,7 +112,7 @@ class ProductSearch(FullTextSearch):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_all_published_items():
|
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):
|
def update_index_for_path(path):
|
||||||
search = ProductSearch(INDEX_NAME)
|
search = ProductSearch(INDEX_NAME)
|
139
erpnext/e_commerce/product_data_engine/filters.py
Normal file
139
erpnext/e_commerce/product_data_engine/filters.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
import frappe
|
||||||
|
from frappe.utils import floor
|
||||||
|
|
||||||
|
|
||||||
|
class ProductFiltersBuilder:
|
||||||
|
def __init__(self, item_group=None):
|
||||||
|
if not item_group:
|
||||||
|
self.doc = frappe.get_doc("E Commerce Settings")
|
||||||
|
else:
|
||||||
|
self.doc = frappe.get_doc("Item Group", item_group)
|
||||||
|
|
||||||
|
self.item_group = item_group
|
||||||
|
|
||||||
|
def get_field_filters(self):
|
||||||
|
if not self.item_group and not self.doc.enable_field_filters:
|
||||||
|
return
|
||||||
|
|
||||||
|
fields, filter_data = [], []
|
||||||
|
filter_fields = [row.fieldname for row in self.doc.filter_fields] # fields in settings
|
||||||
|
|
||||||
|
# filter valid field filters i.e. those that exist in Item
|
||||||
|
item_meta = frappe.get_meta('Item', cached=True)
|
||||||
|
fields = [item_meta.get_field(field) for field in filter_fields if item_meta.has_field(field)]
|
||||||
|
|
||||||
|
for df in fields:
|
||||||
|
item_filters, item_or_filters = {}, []
|
||||||
|
link_doctype_values = self.get_filtered_link_doctype_records(df)
|
||||||
|
|
||||||
|
if df.fieldtype == "Link":
|
||||||
|
if self.item_group:
|
||||||
|
item_or_filters.extend([
|
||||||
|
["item_group", "=", self.item_group],
|
||||||
|
["Website Item Group", "item_group", "=", self.item_group] # consider website item groups
|
||||||
|
])
|
||||||
|
|
||||||
|
# Get link field values attached to published items
|
||||||
|
item_filters['published_in_website'] = 1
|
||||||
|
item_values = frappe.get_all(
|
||||||
|
"Item",
|
||||||
|
fields=[df.fieldname],
|
||||||
|
filters=item_filters,
|
||||||
|
or_filters=item_or_filters,
|
||||||
|
distinct="True",
|
||||||
|
pluck=df.fieldname
|
||||||
|
)
|
||||||
|
|
||||||
|
values = list(set(item_values) & link_doctype_values) # intersection of both
|
||||||
|
else:
|
||||||
|
# table multiselect
|
||||||
|
values = list(link_doctype_values)
|
||||||
|
|
||||||
|
# Remove None
|
||||||
|
if None in values:
|
||||||
|
values.remove(None)
|
||||||
|
|
||||||
|
if values:
|
||||||
|
filter_data.append([df, values])
|
||||||
|
|
||||||
|
return filter_data
|
||||||
|
|
||||||
|
def get_filtered_link_doctype_records(self, field):
|
||||||
|
"""
|
||||||
|
Get valid link doctype records depending on filters.
|
||||||
|
Apply enable/disable/show_in_website filter.
|
||||||
|
Returns:
|
||||||
|
set: A set containing valid record names
|
||||||
|
"""
|
||||||
|
link_doctype = field.get_link_doctype()
|
||||||
|
meta = frappe.get_meta(link_doctype, cached=True) if link_doctype else None
|
||||||
|
if meta:
|
||||||
|
filters = self.get_link_doctype_filters(meta)
|
||||||
|
link_doctype_values = set(d.name for d in frappe.get_all(link_doctype, filters))
|
||||||
|
|
||||||
|
return link_doctype_values if meta else set()
|
||||||
|
|
||||||
|
def get_link_doctype_filters(self, meta):
|
||||||
|
"Filters for Link Doctype eg. 'show_in_website'."
|
||||||
|
filters = {}
|
||||||
|
if not meta:
|
||||||
|
return filters
|
||||||
|
|
||||||
|
if meta.has_field('enabled'):
|
||||||
|
filters['enabled'] = 1
|
||||||
|
if meta.has_field('disabled'):
|
||||||
|
filters['disabled'] = 0
|
||||||
|
if meta.has_field('show_in_website'):
|
||||||
|
filters['show_in_website'] = 1
|
||||||
|
|
||||||
|
return filters
|
||||||
|
|
||||||
|
def get_attribute_filters(self):
|
||||||
|
if not self.item_group and not self.doc.enable_attribute_filters:
|
||||||
|
return
|
||||||
|
|
||||||
|
attributes = [row.attribute for row in self.doc.filter_attributes]
|
||||||
|
|
||||||
|
if not attributes:
|
||||||
|
return []
|
||||||
|
|
||||||
|
result = frappe.get_all(
|
||||||
|
"Item Variant Attribute",
|
||||||
|
filters={
|
||||||
|
"attribute": ["in", attributes],
|
||||||
|
"attribute_value": ["is", "set"]
|
||||||
|
},
|
||||||
|
fields=["attribute", "attribute_value"],
|
||||||
|
distinct=True
|
||||||
|
)
|
||||||
|
|
||||||
|
attribute_value_map = {}
|
||||||
|
for d in result:
|
||||||
|
attribute_value_map.setdefault(d.attribute, []).append(d.attribute_value)
|
||||||
|
|
||||||
|
out = []
|
||||||
|
for name, values in attribute_value_map.items():
|
||||||
|
out.append(frappe._dict(name=name, item_attribute_values=values))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def get_discount_filters(self, discounts):
|
||||||
|
discount_filters = []
|
||||||
|
|
||||||
|
# [25.89, 60.5] min max
|
||||||
|
min_discount, max_discount = discounts[0], discounts[1]
|
||||||
|
# [25, 60] rounded min max
|
||||||
|
min_range_absolute, max_range_absolute = floor(min_discount), floor(max_discount)
|
||||||
|
|
||||||
|
min_range = int(min_discount - (min_range_absolute % 10)) # 20
|
||||||
|
max_range = int(max_discount - (max_range_absolute % 10)) # 60
|
||||||
|
|
||||||
|
min_range = (min_range + 10) if min_range != min_range_absolute else min_range # 30 (upper limit of 25.89 in range of 10)
|
||||||
|
max_range = (max_range + 10) if max_range != max_range_absolute else max_range # 60
|
||||||
|
|
||||||
|
for discount in range(min_range, (max_range + 1), 10):
|
||||||
|
label = f"{discount}% and below"
|
||||||
|
discount_filters.append([discount, label])
|
||||||
|
|
||||||
|
return discount_filters
|
301
erpnext/e_commerce/product_data_engine/query.py
Normal file
301
erpnext/e_commerce/product_data_engine/query.py
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.utils import flt
|
||||||
|
|
||||||
|
from erpnext.e_commerce.doctype.item_review.item_review import get_customer
|
||||||
|
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
|
||||||
|
from erpnext.utilities.product import get_non_stock_item_status
|
||||||
|
|
||||||
|
|
||||||
|
class ProductQuery:
|
||||||
|
"""Query engine for product listing
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
fields (list): Fields to fetch in query
|
||||||
|
conditions (string): Conditions for query building
|
||||||
|
or_conditions (string): Search conditions
|
||||||
|
page_length (Int): Length of page for the query
|
||||||
|
settings (Document): E Commerce Settings DocType
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.settings = frappe.get_doc("E Commerce Settings")
|
||||||
|
self.page_length = self.settings.products_per_page or 20
|
||||||
|
|
||||||
|
self.or_filters = []
|
||||||
|
self.filters = [["published", "=", 1]]
|
||||||
|
self.fields = [
|
||||||
|
"web_item_name", "name", "item_name", "item_code", "website_image",
|
||||||
|
"variant_of", "has_variants", "item_group", "image", "web_long_description",
|
||||||
|
"short_description", "route", "website_warehouse", "ranking", "on_backorder"
|
||||||
|
]
|
||||||
|
|
||||||
|
def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
attributes (dict, optional): Item Attribute filters
|
||||||
|
fields (dict, optional): Field level filters
|
||||||
|
search_term (str, optional): Search term to lookup
|
||||||
|
start (int, optional): Page start
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dict containing items, item count & discount range
|
||||||
|
"""
|
||||||
|
# track if discounts included in field filters
|
||||||
|
self.filter_with_discount = bool(fields.get("discount"))
|
||||||
|
result, discount_list, website_item_groups, cart_items, count = [], [], [], [], 0
|
||||||
|
|
||||||
|
website_item_groups = self.get_website_item_group_results(item_group, website_item_groups)
|
||||||
|
|
||||||
|
if fields:
|
||||||
|
self.build_fields_filters(fields)
|
||||||
|
if search_term:
|
||||||
|
self.build_search_filters(search_term)
|
||||||
|
if self.settings.hide_variants:
|
||||||
|
self.filters.append(["variant_of", "is", "not set"])
|
||||||
|
|
||||||
|
# query results
|
||||||
|
if attributes:
|
||||||
|
result, count = self.query_items_with_attributes(attributes, start)
|
||||||
|
else:
|
||||||
|
result, count = self.query_items(start=start)
|
||||||
|
|
||||||
|
result = self.combine_web_item_group_results(item_group, result, website_item_groups)
|
||||||
|
|
||||||
|
# sort combined results by ranking
|
||||||
|
result = sorted(result, key=lambda x: x.get("ranking"), reverse=True)
|
||||||
|
|
||||||
|
if self.settings.enabled:
|
||||||
|
cart_items = self.get_cart_items()
|
||||||
|
|
||||||
|
result, discount_list = self.add_display_details(result, discount_list, cart_items)
|
||||||
|
|
||||||
|
discounts = []
|
||||||
|
if discount_list:
|
||||||
|
discounts = [min(discount_list), max(discount_list)]
|
||||||
|
|
||||||
|
result = self.filter_results_by_discount(fields, result)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"items": result,
|
||||||
|
"items_count": count,
|
||||||
|
"discounts": discounts
|
||||||
|
}
|
||||||
|
|
||||||
|
def query_items(self, start=0):
|
||||||
|
"""Build a query to fetch Website Items based on field filters."""
|
||||||
|
# MySQL does not support offset without limit,
|
||||||
|
# frappe does not accept two parameters for limit
|
||||||
|
# https://dev.mysql.com/doc/refman/8.0/en/select.html#id4651989
|
||||||
|
count_items = frappe.db.get_all(
|
||||||
|
"Website Item",
|
||||||
|
filters=self.filters,
|
||||||
|
or_filters=self.or_filters,
|
||||||
|
limit_page_length=184467440737095516,
|
||||||
|
limit_start=start, # get all items from this offset for total count ahead
|
||||||
|
order_by="ranking desc")
|
||||||
|
count = len(count_items)
|
||||||
|
|
||||||
|
# If discounts included, return all rows.
|
||||||
|
# Slice after filtering rows with discount (See `filter_results_by_discount`).
|
||||||
|
# Slicing before hand will miss discounted items on the 3rd or 4th page.
|
||||||
|
# Discounts are fetched on computing Pricing Rules so we cannot query them directly.
|
||||||
|
page_length = 184467440737095516 if self.filter_with_discount else self.page_length
|
||||||
|
|
||||||
|
items = frappe.db.get_all(
|
||||||
|
"Website Item",
|
||||||
|
fields=self.fields,
|
||||||
|
filters=self.filters,
|
||||||
|
or_filters=self.or_filters,
|
||||||
|
limit_page_length=page_length,
|
||||||
|
limit_start=start,
|
||||||
|
order_by="ranking desc")
|
||||||
|
|
||||||
|
return items, count
|
||||||
|
|
||||||
|
def query_items_with_attributes(self, attributes, start=0):
|
||||||
|
"""Build a query to fetch Website Items based on field & attribute filters."""
|
||||||
|
item_codes = []
|
||||||
|
|
||||||
|
for attribute, values in attributes.items():
|
||||||
|
if not isinstance(values, list):
|
||||||
|
values = [values]
|
||||||
|
|
||||||
|
# get items that have selected attribute & value
|
||||||
|
item_code_list = frappe.db.get_all(
|
||||||
|
"Item",
|
||||||
|
fields=["item_code"],
|
||||||
|
filters=[
|
||||||
|
["published_in_website", "=", 1],
|
||||||
|
["Item Variant Attribute", "attribute", "=", attribute],
|
||||||
|
["Item Variant Attribute", "attribute_value", "in", values]
|
||||||
|
])
|
||||||
|
item_codes.append({x.item_code for x in item_code_list})
|
||||||
|
|
||||||
|
if item_codes:
|
||||||
|
item_codes = list(set.intersection(*item_codes))
|
||||||
|
self.filters.append(["item_code", "in", item_codes])
|
||||||
|
|
||||||
|
items, count = self.query_items(start=start)
|
||||||
|
|
||||||
|
return items, count
|
||||||
|
|
||||||
|
def build_fields_filters(self, filters):
|
||||||
|
"""Build filters for field values
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filters (dict): Filters
|
||||||
|
"""
|
||||||
|
for field, values in filters.items():
|
||||||
|
if not values or field == "discount":
|
||||||
|
continue
|
||||||
|
|
||||||
|
# handle multiselect fields in filter addition
|
||||||
|
meta = frappe.get_meta('Website Item', cached=True)
|
||||||
|
df = meta.get_field(field)
|
||||||
|
if df.fieldtype == 'Table MultiSelect':
|
||||||
|
child_doctype = df.options
|
||||||
|
child_meta = frappe.get_meta(child_doctype, cached=True)
|
||||||
|
fields = child_meta.get("fields")
|
||||||
|
if fields:
|
||||||
|
self.filters.append([child_doctype, fields[0].fieldname, 'IN', values])
|
||||||
|
elif isinstance(values, list):
|
||||||
|
# If value is a list use `IN` query
|
||||||
|
self.filters.append([field, "in", values])
|
||||||
|
else:
|
||||||
|
# `=` will be faster than `IN` for most cases
|
||||||
|
self.filters.append([field, "=", values])
|
||||||
|
|
||||||
|
def build_search_filters(self, search_term):
|
||||||
|
"""Query search term in specified fields
|
||||||
|
|
||||||
|
Args:
|
||||||
|
search_term (str): Search candidate
|
||||||
|
"""
|
||||||
|
# Default fields to search from
|
||||||
|
default_fields = {'item_code', 'item_name', 'web_long_description', 'item_group'}
|
||||||
|
|
||||||
|
# Get meta search fields
|
||||||
|
meta = frappe.get_meta("Website Item")
|
||||||
|
meta_fields = set(meta.get_search_fields())
|
||||||
|
|
||||||
|
# Join the meta fields and default fields set
|
||||||
|
search_fields = default_fields.union(meta_fields)
|
||||||
|
if frappe.db.count('Website Item', cache=True) > 50000:
|
||||||
|
search_fields.discard('web_long_description')
|
||||||
|
|
||||||
|
# Build or filters for query
|
||||||
|
search = '%{}%'.format(search_term)
|
||||||
|
for field in search_fields:
|
||||||
|
self.or_filters.append([field, "like", search])
|
||||||
|
|
||||||
|
def get_website_item_group_results(self, item_group, website_item_groups):
|
||||||
|
"""Get Web Items for Item Group Page via Website Item Groups."""
|
||||||
|
if item_group:
|
||||||
|
website_item_groups = frappe.db.get_all(
|
||||||
|
"Website Item",
|
||||||
|
fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
|
||||||
|
filters=[
|
||||||
|
["Website Item Group", "item_group", "=", item_group],
|
||||||
|
["published", "=", 1]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return website_item_groups
|
||||||
|
|
||||||
|
def add_display_details(self, result, discount_list, cart_items):
|
||||||
|
"""Add price and availability details in result."""
|
||||||
|
for item in result:
|
||||||
|
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
|
||||||
|
|
||||||
|
if product_info and product_info['price']:
|
||||||
|
# update/mutate item and discount_list objects
|
||||||
|
self.get_price_discount_info(item, product_info['price'], discount_list)
|
||||||
|
|
||||||
|
if self.settings.show_stock_availability:
|
||||||
|
self.get_stock_availability(item)
|
||||||
|
|
||||||
|
item.in_cart = item.item_code in cart_items
|
||||||
|
|
||||||
|
item.wished = False
|
||||||
|
if frappe.db.exists("Wishlist Item", {"item_code": item.item_code, "parent": frappe.session.user}):
|
||||||
|
item.wished = True
|
||||||
|
|
||||||
|
return result, discount_list
|
||||||
|
|
||||||
|
def get_price_discount_info(self, item, price_object, discount_list):
|
||||||
|
"""Modify item object and add price details."""
|
||||||
|
fields = ["formatted_mrp", "formatted_price", "price_list_rate"]
|
||||||
|
for field in fields:
|
||||||
|
item[field] = price_object.get(field)
|
||||||
|
|
||||||
|
if price_object.get('discount_percent'):
|
||||||
|
item.discount_percent = flt(price_object.discount_percent)
|
||||||
|
discount_list.append(price_object.discount_percent)
|
||||||
|
|
||||||
|
if item.formatted_mrp:
|
||||||
|
item.discount = price_object.get('formatted_discount_percent') or \
|
||||||
|
price_object.get('formatted_discount_rate')
|
||||||
|
|
||||||
|
def get_stock_availability(self, item):
|
||||||
|
"""Modify item object and add stock details."""
|
||||||
|
item.in_stock = False
|
||||||
|
warehouse = item.get("website_warehouse")
|
||||||
|
is_stock_item = frappe.get_cached_value("Item", item.item_code, "is_stock_item")
|
||||||
|
|
||||||
|
if item.get("on_backorder"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if not is_stock_item:
|
||||||
|
if warehouse:
|
||||||
|
# product bundle case
|
||||||
|
item.in_stock = get_non_stock_item_status(item.item_code, "website_warehouse")
|
||||||
|
else:
|
||||||
|
item.in_stock = True
|
||||||
|
elif warehouse:
|
||||||
|
# stock item and has warehouse
|
||||||
|
actual_qty = frappe.db.get_value(
|
||||||
|
"Bin",
|
||||||
|
{"item_code": item.item_code,"warehouse": item.get("website_warehouse")},
|
||||||
|
"actual_qty")
|
||||||
|
item.in_stock = bool(flt(actual_qty))
|
||||||
|
|
||||||
|
def get_cart_items(self):
|
||||||
|
customer = get_customer(silent=True)
|
||||||
|
if customer:
|
||||||
|
quotation = frappe.get_all("Quotation", fields=["name"], filters=
|
||||||
|
{"party_name": customer, "order_type": "Shopping Cart", "docstatus": 0},
|
||||||
|
order_by="modified desc", limit_page_length=1)
|
||||||
|
if quotation:
|
||||||
|
items = frappe.get_all(
|
||||||
|
"Quotation Item",
|
||||||
|
fields=["item_code"],
|
||||||
|
filters={
|
||||||
|
"parent": quotation[0].get("name")
|
||||||
|
})
|
||||||
|
items = [row.item_code for row in items]
|
||||||
|
return items
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def combine_web_item_group_results(self, item_group, result, website_item_groups):
|
||||||
|
"""Combine results with context of website item groups into item results."""
|
||||||
|
if item_group and website_item_groups:
|
||||||
|
items_list = {row.name for row in result}
|
||||||
|
for row in website_item_groups:
|
||||||
|
if row.wig_parent not in items_list:
|
||||||
|
result.append(row)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def filter_results_by_discount(self, fields, result):
|
||||||
|
if fields and fields.get("discount"):
|
||||||
|
discount_percent = frappe.utils.flt(fields["discount"][0])
|
||||||
|
result = [row for row in result if row.get("discount_percent") and row.discount_percent <= discount_percent]
|
||||||
|
|
||||||
|
if self.filter_with_discount:
|
||||||
|
# no limit was added to results while querying
|
||||||
|
# slice results manually
|
||||||
|
result[:self.page_length]
|
||||||
|
|
||||||
|
return result
|
@ -0,0 +1,117 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.e_commerce.api import get_product_filter_data
|
||||||
|
from erpnext.e_commerce.doctype.website_item.test_website_item import create_regular_web_item
|
||||||
|
|
||||||
|
test_dependencies = ["Item", "Item Group"]
|
||||||
|
|
||||||
|
class TestItemGroupProductDataEngine(unittest.TestCase):
|
||||||
|
"Test Products & Sub-Category Querying for Product Listing on Item Group Page."
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
item_codes = [
|
||||||
|
("Test Mobile A", "_Test Item Group B"),
|
||||||
|
("Test Mobile B", "_Test Item Group B"),
|
||||||
|
("Test Mobile C", "_Test Item Group B - 1"),
|
||||||
|
("Test Mobile D", "_Test Item Group B - 1"),
|
||||||
|
("Test Mobile E", "_Test Item Group B - 2")
|
||||||
|
]
|
||||||
|
for item in item_codes:
|
||||||
|
item_code = item[0]
|
||||||
|
item_args = {"item_group": item[1]}
|
||||||
|
if not frappe.db.exists("Website Item", {"item_code": item_code}):
|
||||||
|
create_regular_web_item(item_code, item_args=item_args)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
def test_product_listing_in_item_group(self):
|
||||||
|
"Test if only products belonging to the Item Group are fetched."
|
||||||
|
result = get_product_filter_data(query_args={
|
||||||
|
"field_filters": {},
|
||||||
|
"attribute_filters": {},
|
||||||
|
"start": 0,
|
||||||
|
"item_group": "_Test Item Group B"
|
||||||
|
})
|
||||||
|
|
||||||
|
items = result.get("items")
|
||||||
|
item_codes = [item.get("item_code") for item in items]
|
||||||
|
|
||||||
|
self.assertEqual(len(items), 2)
|
||||||
|
self.assertIn("Test Mobile A", item_codes)
|
||||||
|
self.assertNotIn("Test Mobile C", item_codes)
|
||||||
|
|
||||||
|
def test_products_in_multiple_item_groups(self):
|
||||||
|
"""Test if product is visible on multiple item group pages barring its own."""
|
||||||
|
website_item = frappe.get_doc("Website Item", {"item_code": "Test Mobile E"})
|
||||||
|
|
||||||
|
# show item belonging to '_Test Item Group B - 2' in '_Test Item Group B - 1' as well
|
||||||
|
website_item.append("website_item_groups", {
|
||||||
|
"item_group": "_Test Item Group B - 1"
|
||||||
|
})
|
||||||
|
website_item.save()
|
||||||
|
|
||||||
|
result = get_product_filter_data(query_args={
|
||||||
|
"field_filters": {},
|
||||||
|
"attribute_filters": {},
|
||||||
|
"start": 0,
|
||||||
|
"item_group": "_Test Item Group B - 1"
|
||||||
|
})
|
||||||
|
|
||||||
|
items = result.get("items")
|
||||||
|
item_codes = [item.get("item_code") for item in items]
|
||||||
|
|
||||||
|
self.assertEqual(len(items), 3)
|
||||||
|
self.assertIn("Test Mobile E", item_codes) # visible in other item groups
|
||||||
|
self.assertIn("Test Mobile C", item_codes)
|
||||||
|
self.assertIn("Test Mobile D", item_codes)
|
||||||
|
|
||||||
|
result = get_product_filter_data(query_args={
|
||||||
|
"field_filters": {},
|
||||||
|
"attribute_filters": {},
|
||||||
|
"start": 0,
|
||||||
|
"item_group": "_Test Item Group B - 2"
|
||||||
|
})
|
||||||
|
|
||||||
|
items = result.get("items")
|
||||||
|
|
||||||
|
self.assertEqual(len(items), 1)
|
||||||
|
self.assertEqual(items[0].get("item_code"), "Test Mobile E") # visible in own item group
|
||||||
|
|
||||||
|
def test_item_group_with_sub_groups(self):
|
||||||
|
"Test Valid Sub Item Groups in Item Group Page."
|
||||||
|
frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1)
|
||||||
|
frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 0)
|
||||||
|
|
||||||
|
result = get_product_filter_data(query_args={
|
||||||
|
"field_filters": {},
|
||||||
|
"attribute_filters": {},
|
||||||
|
"start": 0,
|
||||||
|
"item_group": "_Test Item Group B"
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertTrue(bool(result.get("sub_categories")))
|
||||||
|
|
||||||
|
child_groups = [d.name for d in result.get("sub_categories")]
|
||||||
|
# check if child group is fetched if shown in website
|
||||||
|
self.assertIn("_Test Item Group B - 1", child_groups)
|
||||||
|
|
||||||
|
frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 1)
|
||||||
|
result = get_product_filter_data(query_args={
|
||||||
|
"field_filters": {},
|
||||||
|
"attribute_filters": {},
|
||||||
|
"start": 0,
|
||||||
|
"item_group": "_Test Item Group B"
|
||||||
|
})
|
||||||
|
child_groups = [d.name for d in result.get("sub_categories")]
|
||||||
|
|
||||||
|
# check if child group is fetched if shown in website
|
||||||
|
self.assertIn("_Test Item Group B - 1", child_groups)
|
||||||
|
self.assertIn("_Test Item Group B - 2", child_groups)
|
@ -0,0 +1,350 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
|
||||||
|
setup_e_commerce_settings,
|
||||||
|
)
|
||||||
|
from erpnext.e_commerce.doctype.website_item.test_website_item import create_regular_web_item
|
||||||
|
from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder
|
||||||
|
from erpnext.e_commerce.product_data_engine.query import ProductQuery
|
||||||
|
|
||||||
|
test_dependencies = ["Item", "Item Group"]
|
||||||
|
|
||||||
|
class TestProductDataEngine(unittest.TestCase):
|
||||||
|
"Test Products Querying and Filters for Product Listing."
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
item_codes = [
|
||||||
|
("Test 11I Laptop", "Products"), # rank 1
|
||||||
|
("Test 12I Laptop", "Products"), # rank 2
|
||||||
|
("Test 13I Laptop", "Products"), # rank 3
|
||||||
|
("Test 14I Laptop", "Raw Material"), # rank 4
|
||||||
|
("Test 15I Laptop", "Raw Material"), # rank 5
|
||||||
|
("Test 16I Laptop", "Raw Material"), # rank 6
|
||||||
|
("Test 17I Laptop", "Products") # rank 7
|
||||||
|
]
|
||||||
|
for index, item in enumerate(item_codes, start=1):
|
||||||
|
item_code = item[0]
|
||||||
|
item_args = {"item_group": item[1]}
|
||||||
|
web_args = {"ranking": index}
|
||||||
|
if not frappe.db.exists("Website Item", {"item_code": item_code}):
|
||||||
|
create_regular_web_item(item_code, item_args=item_args, web_args=web_args)
|
||||||
|
|
||||||
|
setup_e_commerce_settings({
|
||||||
|
"products_per_page": 4,
|
||||||
|
"enable_field_filters": 1,
|
||||||
|
"filter_fields": [{"fieldname": "item_group"}],
|
||||||
|
"enable_attribute_filters": 1,
|
||||||
|
"filter_attributes": [{"attribute": "Test Size"}],
|
||||||
|
"company": "_Test Company",
|
||||||
|
"enabled": 1,
|
||||||
|
"default_customer_group": "_Test Customer Group",
|
||||||
|
"price_list": "_Test Price List India"
|
||||||
|
})
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
def test_product_list_ordering_and_paging(self):
|
||||||
|
"Test if website items appear by ranking on different pages."
|
||||||
|
engine = ProductQuery()
|
||||||
|
result = engine.query(
|
||||||
|
attributes={},
|
||||||
|
fields={},
|
||||||
|
search_term=None,
|
||||||
|
start=0,
|
||||||
|
item_group=None
|
||||||
|
)
|
||||||
|
items = result.get("items")
|
||||||
|
|
||||||
|
self.assertIsNotNone(items)
|
||||||
|
self.assertEqual(len(items), 4)
|
||||||
|
self.assertGreater(result.get("items_count"), 4)
|
||||||
|
|
||||||
|
# check if items appear as per ranking set in setUpClass
|
||||||
|
self.assertEqual(items[0].get("item_code"), "Test 17I Laptop")
|
||||||
|
self.assertEqual(items[1].get("item_code"), "Test 16I Laptop")
|
||||||
|
self.assertEqual(items[2].get("item_code"), "Test 15I Laptop")
|
||||||
|
self.assertEqual(items[3].get("item_code"), "Test 14I Laptop")
|
||||||
|
|
||||||
|
# check next page
|
||||||
|
result = engine.query(
|
||||||
|
attributes={},
|
||||||
|
fields={},
|
||||||
|
search_term=None,
|
||||||
|
start=4,
|
||||||
|
item_group=None
|
||||||
|
)
|
||||||
|
items = result.get("items")
|
||||||
|
|
||||||
|
# check if items appear as per ranking set in setUpClass on next page
|
||||||
|
self.assertEqual(items[0].get("item_code"), "Test 13I Laptop")
|
||||||
|
self.assertEqual(items[1].get("item_code"), "Test 12I Laptop")
|
||||||
|
self.assertEqual(items[2].get("item_code"), "Test 11I Laptop")
|
||||||
|
|
||||||
|
def test_change_product_ranking(self):
|
||||||
|
"Test if item on second page appear on first if ranking is changed."
|
||||||
|
item_code = "Test 12I Laptop"
|
||||||
|
old_ranking = frappe.db.get_value("Website Item", {"item_code": item_code}, "ranking")
|
||||||
|
|
||||||
|
# low rank, appears on second page
|
||||||
|
self.assertEqual(old_ranking, 2)
|
||||||
|
|
||||||
|
# set ranking as highest rank
|
||||||
|
frappe.db.set_value("Website Item", {"item_code": item_code}, "ranking", 10)
|
||||||
|
|
||||||
|
engine = ProductQuery()
|
||||||
|
result = engine.query(
|
||||||
|
attributes={},
|
||||||
|
fields={},
|
||||||
|
search_term=None,
|
||||||
|
start=0,
|
||||||
|
item_group=None
|
||||||
|
)
|
||||||
|
items = result.get("items")
|
||||||
|
|
||||||
|
# check if item is the first item on the first page
|
||||||
|
self.assertEqual(items[0].get("item_code"), item_code)
|
||||||
|
self.assertEqual(items[1].get("item_code"), "Test 17I Laptop")
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
frappe.db.set_value("Website Item", {"item_code": item_code}, "ranking", old_ranking)
|
||||||
|
|
||||||
|
def test_product_list_field_filter_builder(self):
|
||||||
|
"Test if field filters are fetched correctly."
|
||||||
|
frappe.db.set_value("Item Group", "Raw Material", "show_in_website", 0)
|
||||||
|
|
||||||
|
filter_engine = ProductFiltersBuilder()
|
||||||
|
field_filters = filter_engine.get_field_filters()
|
||||||
|
|
||||||
|
# Web Items belonging to 'Products' and 'Raw Material' are available
|
||||||
|
# but only 'Products' has 'show_in_website' enabled
|
||||||
|
item_group_filters = field_filters[0]
|
||||||
|
docfield = item_group_filters[0]
|
||||||
|
valid_item_groups = item_group_filters[1]
|
||||||
|
|
||||||
|
self.assertEqual(docfield.options, "Item Group")
|
||||||
|
self.assertIn("Products", valid_item_groups)
|
||||||
|
self.assertNotIn("Raw Material", valid_item_groups)
|
||||||
|
|
||||||
|
frappe.db.set_value("Item Group", "Raw Material", "show_in_website", 1)
|
||||||
|
field_filters = filter_engine.get_field_filters()
|
||||||
|
|
||||||
|
#'Products' and 'Raw Materials' both have 'show_in_website' enabled
|
||||||
|
item_group_filters = field_filters[0]
|
||||||
|
docfield = item_group_filters[0]
|
||||||
|
valid_item_groups = item_group_filters[1]
|
||||||
|
|
||||||
|
self.assertEqual(docfield.options, "Item Group")
|
||||||
|
self.assertIn("Products", valid_item_groups)
|
||||||
|
self.assertIn("Raw Material", valid_item_groups)
|
||||||
|
|
||||||
|
def test_product_list_with_field_filter(self):
|
||||||
|
"Test if field filters are applied correctly."
|
||||||
|
field_filters = {"item_group": "Raw Material"}
|
||||||
|
|
||||||
|
engine = ProductQuery()
|
||||||
|
result = engine.query(
|
||||||
|
attributes={},
|
||||||
|
fields=field_filters,
|
||||||
|
search_term=None,
|
||||||
|
start=0,
|
||||||
|
item_group=None
|
||||||
|
)
|
||||||
|
items = result.get("items")
|
||||||
|
|
||||||
|
# check if only 'Raw Material' are fetched in the right order
|
||||||
|
self.assertEqual(len(items), 3)
|
||||||
|
self.assertEqual(items[0].get("item_code"), "Test 16I Laptop")
|
||||||
|
self.assertEqual(items[1].get("item_code"), "Test 15I Laptop")
|
||||||
|
|
||||||
|
# def test_product_list_with_field_filter_table_multiselect(self):
|
||||||
|
# TODO
|
||||||
|
# pass
|
||||||
|
|
||||||
|
def test_product_list_attribute_filter_builder(self):
|
||||||
|
"Test if attribute filters are fetched correctly."
|
||||||
|
create_variant_web_item()
|
||||||
|
|
||||||
|
filter_engine = ProductFiltersBuilder()
|
||||||
|
attribute_filter = filter_engine.get_attribute_filters()[0]
|
||||||
|
attribute_values = attribute_filter.item_attribute_values
|
||||||
|
|
||||||
|
self.assertEqual(attribute_filter.name, "Test Size")
|
||||||
|
self.assertGreater(len(attribute_values), 0)
|
||||||
|
self.assertIn("Large", attribute_values)
|
||||||
|
|
||||||
|
def test_product_list_with_attribute_filter(self):
|
||||||
|
"Test if attribute filters are applied correctly."
|
||||||
|
create_variant_web_item()
|
||||||
|
|
||||||
|
attribute_filters = {"Test Size": ["Large"]}
|
||||||
|
engine = ProductQuery()
|
||||||
|
result = engine.query(
|
||||||
|
attributes=attribute_filters,
|
||||||
|
fields={},
|
||||||
|
search_term=None,
|
||||||
|
start=0,
|
||||||
|
item_group=None
|
||||||
|
)
|
||||||
|
items = result.get("items")
|
||||||
|
|
||||||
|
# check if only items with Test Size 'Large' are fetched
|
||||||
|
self.assertEqual(len(items), 1)
|
||||||
|
self.assertEqual(items[0].get("item_code"), "Test Web Item-L")
|
||||||
|
|
||||||
|
def test_product_list_discount_filter_builder(self):
|
||||||
|
"Test if discount filters are fetched correctly."
|
||||||
|
from erpnext.e_commerce.doctype.website_item.test_website_item import (
|
||||||
|
make_web_item_price,
|
||||||
|
make_web_pricing_rule,
|
||||||
|
)
|
||||||
|
|
||||||
|
item_code = "Test 12I Laptop"
|
||||||
|
make_web_item_price(item_code=item_code)
|
||||||
|
make_web_pricing_rule(
|
||||||
|
title=f"Test Pricing Rule for {item_code}",
|
||||||
|
item_code=item_code,
|
||||||
|
selling=1
|
||||||
|
)
|
||||||
|
|
||||||
|
setup_e_commerce_settings({"show_price": 1})
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
|
||||||
|
|
||||||
|
engine = ProductQuery()
|
||||||
|
result = engine.query(
|
||||||
|
attributes={},
|
||||||
|
fields={},
|
||||||
|
search_term=None,
|
||||||
|
start=4,
|
||||||
|
item_group=None
|
||||||
|
)
|
||||||
|
self.assertTrue(bool(result.get("discounts")))
|
||||||
|
|
||||||
|
filter_engine = ProductFiltersBuilder()
|
||||||
|
discount_filters = filter_engine.get_discount_filters(result["discounts"])
|
||||||
|
|
||||||
|
self.assertEqual(len(discount_filters[0]), 2)
|
||||||
|
self.assertEqual(discount_filters[0][0], 10)
|
||||||
|
self.assertEqual(discount_filters[0][1], "10% and below")
|
||||||
|
|
||||||
|
def test_product_list_with_discount_filters(self):
|
||||||
|
"Test if discount filters are applied correctly."
|
||||||
|
from erpnext.e_commerce.doctype.website_item.test_website_item import (
|
||||||
|
make_web_item_price,
|
||||||
|
make_web_pricing_rule,
|
||||||
|
)
|
||||||
|
|
||||||
|
field_filters = {"discount": [10]}
|
||||||
|
|
||||||
|
make_web_item_price(item_code="Test 12I Laptop")
|
||||||
|
make_web_pricing_rule(
|
||||||
|
title="Test Pricing Rule for Test 12I Laptop", # 10% discount
|
||||||
|
item_code="Test 12I Laptop",
|
||||||
|
selling=1
|
||||||
|
)
|
||||||
|
make_web_item_price(item_code="Test 13I Laptop")
|
||||||
|
make_web_pricing_rule(
|
||||||
|
title="Test Pricing Rule for Test 13I Laptop", # 15% discount
|
||||||
|
item_code="Test 13I Laptop",
|
||||||
|
discount_percentage=15,
|
||||||
|
selling=1
|
||||||
|
)
|
||||||
|
|
||||||
|
setup_e_commerce_settings({"show_price": 1})
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
|
||||||
|
engine = ProductQuery()
|
||||||
|
result = engine.query(
|
||||||
|
attributes={},
|
||||||
|
fields=field_filters,
|
||||||
|
search_term=None,
|
||||||
|
start=0,
|
||||||
|
item_group=None
|
||||||
|
)
|
||||||
|
items = result.get("items")
|
||||||
|
|
||||||
|
# check if only product with 10% and below discount are fetched
|
||||||
|
self.assertEqual(len(items), 1)
|
||||||
|
self.assertEqual(items[0].get("item_code"), "Test 12I Laptop")
|
||||||
|
|
||||||
|
def test_product_list_with_api(self):
|
||||||
|
"Test products listing using API."
|
||||||
|
from erpnext.e_commerce.api import get_product_filter_data
|
||||||
|
|
||||||
|
create_variant_web_item()
|
||||||
|
|
||||||
|
result = get_product_filter_data(query_args={
|
||||||
|
"field_filters": {
|
||||||
|
"item_group": "Products"
|
||||||
|
},
|
||||||
|
"attribute_filters": {
|
||||||
|
"Test Size": ["Large"]
|
||||||
|
},
|
||||||
|
"start": 0
|
||||||
|
})
|
||||||
|
|
||||||
|
items = result.get("items")
|
||||||
|
|
||||||
|
self.assertEqual(len(items), 1)
|
||||||
|
self.assertEqual(items[0].get("item_code"), "Test Web Item-L")
|
||||||
|
|
||||||
|
def test_product_list_with_variants(self):
|
||||||
|
"Test if variants are hideen on hiding variants in settings."
|
||||||
|
create_variant_web_item()
|
||||||
|
|
||||||
|
setup_e_commerce_settings({
|
||||||
|
"enable_attribute_filters": 0,
|
||||||
|
"hide_variants": 1
|
||||||
|
})
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
|
||||||
|
attribute_filters = {"Test Size": ["Large"]}
|
||||||
|
engine = ProductQuery()
|
||||||
|
result = engine.query(
|
||||||
|
attributes=attribute_filters,
|
||||||
|
fields={},
|
||||||
|
search_term=None,
|
||||||
|
start=0,
|
||||||
|
item_group=None
|
||||||
|
)
|
||||||
|
items = result.get("items")
|
||||||
|
|
||||||
|
# check if any variants are fetched even though published variant exists
|
||||||
|
self.assertEqual(len(items), 0)
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
setup_e_commerce_settings({
|
||||||
|
"enable_attribute_filters": 1,
|
||||||
|
"hide_variants": 0
|
||||||
|
})
|
||||||
|
|
||||||
|
def create_variant_web_item():
|
||||||
|
"Create Variant and Template Website Items."
|
||||||
|
from erpnext.controllers.item_variant import create_variant
|
||||||
|
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
|
make_item("Test Web Item", {
|
||||||
|
"has_variant": 1,
|
||||||
|
"variant_based_on": "Item Attribute",
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"attribute": "Test Size"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
if not frappe.db.exists("Item", "Test Web Item-L"):
|
||||||
|
variant = create_variant("Test Web Item", {"Test Size": "Large"})
|
||||||
|
variant.save()
|
||||||
|
|
||||||
|
if not frappe.db.exists("Website Item", {"variant_of": "Test Web Item"}):
|
||||||
|
make_website_item(variant, save=True)
|
201
erpnext/e_commerce/product_ui/grid.js
Normal file
201
erpnext/e_commerce/product_ui/grid.js
Normal 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 ``;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
204
erpnext/e_commerce/product_ui/list.js
Normal file
204
erpnext/e_commerce/product_ui/list.js
Normal 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 ``;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
244
erpnext/e_commerce/product_ui/search.js
Normal file
244
erpnext/e_commerce/product_ui/search.js
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
erpnext.ProductSearch = class {
|
||||||
|
constructor(opts) {
|
||||||
|
/* Options: search_box_id (for custom search box) */
|
||||||
|
$.extend(this, opts);
|
||||||
|
this.MAX_RECENT_SEARCHES = 4;
|
||||||
|
this.search_box_id = this.search_box_id || "#search-box";
|
||||||
|
this.searchBox = $(this.search_box_id);
|
||||||
|
|
||||||
|
this.setupSearchDropDown();
|
||||||
|
this.bindSearchAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupSearchDropDown() {
|
||||||
|
this.search_area = $("#dropdownMenuSearch");
|
||||||
|
this.setupSearchResultContainer();
|
||||||
|
this.populateRecentSearches();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindSearchAction() {
|
||||||
|
let me = this;
|
||||||
|
|
||||||
|
// Show Search dropdown
|
||||||
|
this.searchBox.on("focus", () => {
|
||||||
|
this.search_dropdown.removeClass("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
// If click occurs outside search input/results, hide results.
|
||||||
|
// Click can happen anywhere on the page
|
||||||
|
$("body").on("click", (e) => {
|
||||||
|
let searchEvent = $(e.target).closest(this.search_box_id).length;
|
||||||
|
let resultsEvent = $(e.target).closest('#search-results-container').length;
|
||||||
|
let isResultHidden = this.search_dropdown.hasClass("hidden");
|
||||||
|
|
||||||
|
if (!searchEvent && !resultsEvent && !isResultHidden) {
|
||||||
|
this.search_dropdown.addClass("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process search input
|
||||||
|
this.searchBox.on("input", (e) => {
|
||||||
|
let query = e.target.value;
|
||||||
|
|
||||||
|
if (query.length == 0) {
|
||||||
|
me.populateResults(null);
|
||||||
|
me.populateCategoriesList(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.length < 3 || !query.length) return;
|
||||||
|
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.templates.pages.product_search.search",
|
||||||
|
args: {
|
||||||
|
query: query
|
||||||
|
},
|
||||||
|
callback: (data) => {
|
||||||
|
let product_results = null, category_results = null;
|
||||||
|
|
||||||
|
// Populate product results
|
||||||
|
product_results = data.message ? data.message.product_results : null;
|
||||||
|
me.populateResults(product_results);
|
||||||
|
|
||||||
|
// Populate categories
|
||||||
|
if (me.category_container) {
|
||||||
|
category_results = data.message ? data.message.category_results : null;
|
||||||
|
me.populateCategoriesList(category_results);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate recent search chips only on successful queries
|
||||||
|
if (!$.isEmptyObject(product_results) || !$.isEmptyObject(category_results)) {
|
||||||
|
me.setRecentSearches(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.search_dropdown.removeClass("hidden");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupSearchResultContainer() {
|
||||||
|
this.search_dropdown = this.search_area.append(`
|
||||||
|
<div class="overflow-hidden shadow dropdown-menu w-100 hidden"
|
||||||
|
id="search-results-container"
|
||||||
|
aria-labelledby="dropdownMenuSearch"
|
||||||
|
style="display: flex; flex-direction: column;">
|
||||||
|
</div>
|
||||||
|
`).find("#search-results-container");
|
||||||
|
|
||||||
|
this.setupCategoryContainer();
|
||||||
|
this.setupProductsContainer();
|
||||||
|
this.setupRecentsContainer();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupProductsContainer() {
|
||||||
|
this.products_container = this.search_dropdown.append(`
|
||||||
|
<div id="product-results mt-2">
|
||||||
|
<div id="product-scroll" style="overflow: scroll; max-height: 300px">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).find("#product-scroll");
|
||||||
|
}
|
||||||
|
|
||||||
|
setupCategoryContainer() {
|
||||||
|
this.category_container = this.search_dropdown.append(`
|
||||||
|
<div class="category-container mt-2 mb-1">
|
||||||
|
<div class="category-chips">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).find(".category-chips");
|
||||||
|
}
|
||||||
|
|
||||||
|
setupRecentsContainer() {
|
||||||
|
let $recents_section = this.search_dropdown.append(`
|
||||||
|
<div class="mb-2 mt-2 recent-searches">
|
||||||
|
<div>
|
||||||
|
<b>${ __("Recent") }</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).find(".recent-searches");
|
||||||
|
|
||||||
|
this.recents_container = $recents_section.append(`
|
||||||
|
<div id="recents" style="padding: .25rem 0 1rem 0;">
|
||||||
|
</div>
|
||||||
|
`).find("#recents");
|
||||||
|
}
|
||||||
|
|
||||||
|
getRecentSearches() {
|
||||||
|
return JSON.parse(localStorage.getItem("recent_searches") || "[]");
|
||||||
|
}
|
||||||
|
|
||||||
|
attachEventListenersToChips() {
|
||||||
|
let me = this;
|
||||||
|
const chips = $(".recent-search");
|
||||||
|
window.chips = chips;
|
||||||
|
|
||||||
|
for (let chip of chips) {
|
||||||
|
chip.addEventListener("click", () => {
|
||||||
|
me.searchBox[0].value = chip.innerText.trim();
|
||||||
|
|
||||||
|
// Start search with `recent query`
|
||||||
|
me.searchBox.trigger("input");
|
||||||
|
me.searchBox.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRecentSearches(query) {
|
||||||
|
let recents = this.getRecentSearches();
|
||||||
|
if (recents.length >= this.MAX_RECENT_SEARCHES) {
|
||||||
|
// Remove the `first` query
|
||||||
|
recents.splice(0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recents.indexOf(query) >= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
recents.push(query);
|
||||||
|
localStorage.setItem("recent_searches", JSON.stringify(recents));
|
||||||
|
|
||||||
|
this.populateRecentSearches();
|
||||||
|
}
|
||||||
|
|
||||||
|
populateRecentSearches() {
|
||||||
|
let recents = this.getRecentSearches();
|
||||||
|
|
||||||
|
if (!recents.length) {
|
||||||
|
this.recents_container.html(`<span class=""text-muted">No searches yet.</span>`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = "";
|
||||||
|
recents.forEach((key) => {
|
||||||
|
html += `
|
||||||
|
<div class="recent-search mr-1" style="font-size: 13px">
|
||||||
|
<span class="mr-2">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="var(--gray-500)"" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M8.00027 5.20947V8.00017L10 10" stroke="var(--gray-500)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
${ key }
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.recents_container.html(html);
|
||||||
|
this.attachEventListenersToChips();
|
||||||
|
}
|
||||||
|
|
||||||
|
populateResults(product_results) {
|
||||||
|
if (!product_results || product_results.length === 0) {
|
||||||
|
let empty_html = ``;
|
||||||
|
this.products_container.html(empty_html);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = "";
|
||||||
|
|
||||||
|
product_results.forEach((res) => {
|
||||||
|
let thumbnail = res.thumbnail || '/assets/erpnext/images/ui-states/cart-empty-state.png';
|
||||||
|
html += `
|
||||||
|
<div class="dropdown-item" style="display: flex;">
|
||||||
|
<img class="item-thumb col-2" src=${thumbnail} />
|
||||||
|
<div class="col-9" style="white-space: normal;">
|
||||||
|
<a href="/${res.route}">${res.web_item_name}</a><br>
|
||||||
|
<span class="brand-line">${res.brand ? "by " + res.brand : ""}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.products_container.html(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
populateCategoriesList(category_results) {
|
||||||
|
if (!category_results || category_results.length === 0) {
|
||||||
|
let empty_html = `
|
||||||
|
<div class="category-container mt-2">
|
||||||
|
<div class="category-chips">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.category_container.html(empty_html);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="mb-2">
|
||||||
|
<b>${ __("Categories") }</b>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
category_results.forEach((category) => {
|
||||||
|
html += `
|
||||||
|
<a href="/${category.route}" class="btn btn-sm category-chip mr-2 mb-2"
|
||||||
|
style="font-size: 13px" role="button">
|
||||||
|
${ category.name }
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.category_container.html(html);
|
||||||
|
}
|
||||||
|
};
|
532
erpnext/e_commerce/product_ui/views.js
Normal file
532
erpnext/e_commerce/product_ui/views.js
Normal file
@ -0,0 +1,532 @@
|
|||||||
|
erpnext.ProductView = class {
|
||||||
|
/* Options:
|
||||||
|
- View Type
|
||||||
|
- Products Section Wrapper,
|
||||||
|
- Item Group: If its an Item Group page
|
||||||
|
*/
|
||||||
|
constructor(options) {
|
||||||
|
Object.assign(this, options);
|
||||||
|
this.preference = this.view_type;
|
||||||
|
this.make();
|
||||||
|
}
|
||||||
|
|
||||||
|
make(from_filters=false) {
|
||||||
|
this.products_section.empty();
|
||||||
|
this.prepare_toolbar();
|
||||||
|
this.get_item_filter_data(from_filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_toolbar() {
|
||||||
|
this.products_section.append(`
|
||||||
|
<div class="toolbar d-flex">
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
this.prepare_search();
|
||||||
|
this.prepare_view_toggler();
|
||||||
|
|
||||||
|
new erpnext.ProductSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_view_toggler() {
|
||||||
|
|
||||||
|
if (!$("#list").length || !$("#image-view").length) {
|
||||||
|
this.render_view_toggler();
|
||||||
|
this.bind_view_toggler_actions();
|
||||||
|
this.set_view_state();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get_item_filter_data(from_filters=false) {
|
||||||
|
// Get and render all Product related views
|
||||||
|
let me = this;
|
||||||
|
this.from_filters = from_filters;
|
||||||
|
let args = this.get_query_filters();
|
||||||
|
|
||||||
|
this.disable_view_toggler(true);
|
||||||
|
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.e_commerce.api.get_product_filter_data",
|
||||||
|
args: {
|
||||||
|
query_args: args
|
||||||
|
},
|
||||||
|
callback: function(result) {
|
||||||
|
if (!result || result.exc || !result.message || result.message.exc) {
|
||||||
|
me.render_no_products_section(true);
|
||||||
|
} else {
|
||||||
|
// Sub Category results are independent of Items
|
||||||
|
if (me.item_group && result.message["sub_categories"].length) {
|
||||||
|
me.render_item_sub_categories(result.message["sub_categories"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.message["items"].length) {
|
||||||
|
// if result has no items or result is empty
|
||||||
|
me.render_no_products_section();
|
||||||
|
} else {
|
||||||
|
// Add discount filters
|
||||||
|
me.re_render_discount_filters(result.message["filters"].discount_filters);
|
||||||
|
|
||||||
|
// Render views
|
||||||
|
me.render_list_view(result.message["items"], result.message["settings"]);
|
||||||
|
me.render_grid_view(result.message["items"], result.message["settings"]);
|
||||||
|
|
||||||
|
me.products = result.message["items"];
|
||||||
|
me.product_count = result.message["items_count"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind filter actions
|
||||||
|
if (!from_filters) {
|
||||||
|
// If `get_product_filter_data` was triggered after checking a filter,
|
||||||
|
// don't touch filters unnecessarily, only data must change
|
||||||
|
// filter persistence is handle on filter change event
|
||||||
|
me.bind_filters();
|
||||||
|
me.restore_filters_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom paging
|
||||||
|
me.add_paging_section(result.message["settings"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
me.disable_view_toggler(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
disable_view_toggler(disable=false) {
|
||||||
|
$('#list').prop('disabled', disable);
|
||||||
|
$('#image-view').prop('disabled', disable);
|
||||||
|
}
|
||||||
|
|
||||||
|
render_grid_view(items, settings) {
|
||||||
|
// loop over data and add grid html to it
|
||||||
|
let me = this;
|
||||||
|
this.prepare_product_area_wrapper("grid");
|
||||||
|
|
||||||
|
new erpnext.ProductGrid({
|
||||||
|
items: items,
|
||||||
|
products_section: $("#products-grid-area"),
|
||||||
|
settings: settings,
|
||||||
|
preference: me.preference
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render_list_view(items, settings) {
|
||||||
|
let me = this;
|
||||||
|
this.prepare_product_area_wrapper("list");
|
||||||
|
|
||||||
|
new erpnext.ProductList({
|
||||||
|
items: items,
|
||||||
|
products_section: $("#products-list-area"),
|
||||||
|
settings: settings,
|
||||||
|
preference: me.preference
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_product_area_wrapper(view) {
|
||||||
|
let left_margin = view == "list" ? "ml-2" : "";
|
||||||
|
let top_margin = view == "list" ? "mt-6" : "mt-minus-1";
|
||||||
|
return this.products_section.append(`
|
||||||
|
<br>
|
||||||
|
<div id="products-${view}-area" class="row products-list ${ top_margin } ${ left_margin }"></div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
get_query_filters() {
|
||||||
|
const filters = frappe.utils.get_query_params();
|
||||||
|
let {field_filters, attribute_filters} = filters;
|
||||||
|
|
||||||
|
field_filters = field_filters ? JSON.parse(field_filters) : {};
|
||||||
|
attribute_filters = attribute_filters ? JSON.parse(attribute_filters) : {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
field_filters: field_filters,
|
||||||
|
attribute_filters: attribute_filters,
|
||||||
|
item_group: this.item_group,
|
||||||
|
start: filters.start || null,
|
||||||
|
from_filters: this.from_filters || false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
add_paging_section(settings) {
|
||||||
|
$(".product-paging-area").remove();
|
||||||
|
|
||||||
|
if (this.products) {
|
||||||
|
let paging_html = `
|
||||||
|
<div class="row product-paging-area mt-5">
|
||||||
|
<div class="col-3">
|
||||||
|
</div>
|
||||||
|
<div class="col-9 text-right">
|
||||||
|
`;
|
||||||
|
let query_params = frappe.utils.get_query_params();
|
||||||
|
let start = query_params.start ? cint(JSON.parse(query_params.start)) : 0;
|
||||||
|
let page_length = settings.products_per_page || 0;
|
||||||
|
|
||||||
|
let prev_disable = start > 0 ? "" : "disabled";
|
||||||
|
let next_disable = (this.product_count > page_length) ? "" : "disabled";
|
||||||
|
|
||||||
|
paging_html += `
|
||||||
|
<button class="btn btn-default btn-prev" data-start="${ start - page_length }"
|
||||||
|
style="float: left" ${prev_disable}>
|
||||||
|
${ __("Prev") }
|
||||||
|
</button>`;
|
||||||
|
|
||||||
|
paging_html += `
|
||||||
|
<button class="btn btn-default btn-next" data-start="${ start + page_length }"
|
||||||
|
${next_disable}>
|
||||||
|
${ __("Next") }
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
paging_html += `</div></div>`;
|
||||||
|
|
||||||
|
$(".page_content").append(paging_html);
|
||||||
|
this.bind_paging_action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_search() {
|
||||||
|
$(".toolbar").append(`
|
||||||
|
<div class="input-group col-8 p-0">
|
||||||
|
<div class="dropdown w-100" id="dropdownMenuSearch">
|
||||||
|
<input type="search" name="query" id="search-box" class="form-control font-md"
|
||||||
|
placeholder="Search for Products"
|
||||||
|
aria-label="Product" aria-describedby="button-addon2">
|
||||||
|
<div class="search-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="feather feather-search">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<!-- Results dropdown rendered in product_search.js -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
render_view_toggler() {
|
||||||
|
$(".toolbar").append(`<div class="toggle-container col-4 p-0"></div>`);
|
||||||
|
|
||||||
|
["btn-list-view", "btn-grid-view"].forEach(view => {
|
||||||
|
let icon = view === "btn-list-view" ? "list" : "image-view";
|
||||||
|
$(".toggle-container").append(`
|
||||||
|
<div class="form-group mb-0" id="toggle-view">
|
||||||
|
<button id="${ icon }" class="btn ${ view } mr-2">
|
||||||
|
<span>
|
||||||
|
<svg class="icon icon-md">
|
||||||
|
<use href="#icon-${ icon }"></use>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bind_view_toggler_actions() {
|
||||||
|
$("#list").click(function() {
|
||||||
|
let $btn = $(this);
|
||||||
|
$btn.removeClass('btn-primary');
|
||||||
|
$btn.addClass('btn-primary');
|
||||||
|
$(".btn-grid-view").removeClass('btn-primary');
|
||||||
|
|
||||||
|
$("#products-grid-area").addClass("hidden");
|
||||||
|
$("#products-list-area").removeClass("hidden");
|
||||||
|
localStorage.setItem("product_view", "List View");
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#image-view").click(function() {
|
||||||
|
let $btn = $(this);
|
||||||
|
$btn.removeClass('btn-primary');
|
||||||
|
$btn.addClass('btn-primary');
|
||||||
|
$(".btn-list-view").removeClass('btn-primary');
|
||||||
|
|
||||||
|
$("#products-list-area").addClass("hidden");
|
||||||
|
$("#products-grid-area").removeClass("hidden");
|
||||||
|
localStorage.setItem("product_view", "Grid View");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
set_view_state() {
|
||||||
|
if (this.preference === "List View") {
|
||||||
|
$("#list").addClass('btn-primary');
|
||||||
|
$("#image-view").removeClass('btn-primary');
|
||||||
|
} else {
|
||||||
|
$("#image-view").addClass('btn-primary');
|
||||||
|
$("#list").removeClass('btn-primary');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bind_paging_action() {
|
||||||
|
let me = this;
|
||||||
|
$('.btn-prev, .btn-next').click((e) => {
|
||||||
|
const $btn = $(e.target);
|
||||||
|
me.from_filters = false;
|
||||||
|
|
||||||
|
$btn.prop('disabled', true);
|
||||||
|
const start = $btn.data('start');
|
||||||
|
|
||||||
|
let query_params = frappe.utils.get_query_params();
|
||||||
|
query_params.start = start;
|
||||||
|
let path = window.location.pathname + '?' + frappe.utils.get_url_from_dict(query_params);
|
||||||
|
window.location.href = path;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
re_render_discount_filters(filter_data) {
|
||||||
|
this.get_discount_filter_html(filter_data);
|
||||||
|
if (this.from_filters) {
|
||||||
|
// Bind filter action if triggered via filters
|
||||||
|
// if not from filter action, page load will bind actions
|
||||||
|
this.bind_discount_filter_action();
|
||||||
|
}
|
||||||
|
// discount filters are rendered with Items (later)
|
||||||
|
// unlike the other filters
|
||||||
|
this.restore_discount_filter();
|
||||||
|
}
|
||||||
|
|
||||||
|
get_discount_filter_html(filter_data) {
|
||||||
|
$("#discount-filters").remove();
|
||||||
|
if (filter_data) {
|
||||||
|
$("#product-filters").append(`
|
||||||
|
<div id="discount-filters" class="mb-4 filter-block pb-5">
|
||||||
|
<div class="filter-label mb-3">${ __("Discounts") }</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
let html = `<div class="filter-options">`;
|
||||||
|
filter_data.forEach(filter => {
|
||||||
|
html += `
|
||||||
|
<div class="checkbox">
|
||||||
|
<label data-value="${ filter[0] }">
|
||||||
|
<input type="radio"
|
||||||
|
class="product-filter discount-filter"
|
||||||
|
name="discount" id="${ filter[0] }"
|
||||||
|
data-filter-name="discount"
|
||||||
|
data-filter-value="${ filter[0] }"
|
||||||
|
style="width: 14px !important"
|
||||||
|
>
|
||||||
|
<span class="label-area" for="${ filter[0] }">
|
||||||
|
${ filter[1] }
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
html += `</div>`;
|
||||||
|
|
||||||
|
$("#discount-filters").append(html);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_discount_filter() {
|
||||||
|
const filters = frappe.utils.get_query_params();
|
||||||
|
let field_filters = filters.field_filters;
|
||||||
|
if (!field_filters) return;
|
||||||
|
|
||||||
|
field_filters = JSON.parse(field_filters);
|
||||||
|
|
||||||
|
if (field_filters && field_filters["discount"]) {
|
||||||
|
const values = field_filters["discount"];
|
||||||
|
const selector = values.map(value => {
|
||||||
|
return `input[data-filter-name="discount"][data-filter-value="${value}"]`;
|
||||||
|
}).join(',');
|
||||||
|
$(selector).prop('checked', true);
|
||||||
|
this.field_filters = field_filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bind_discount_filter_action() {
|
||||||
|
let me = this;
|
||||||
|
$('.discount-filter').on('change', (e) => {
|
||||||
|
const $checkbox = $(e.target);
|
||||||
|
const is_checked = $checkbox.is(':checked');
|
||||||
|
|
||||||
|
const {
|
||||||
|
filterValue: filter_value
|
||||||
|
} = $checkbox.data();
|
||||||
|
|
||||||
|
delete this.field_filters["discount"];
|
||||||
|
|
||||||
|
if (is_checked) {
|
||||||
|
this.field_filters["discount"] = [];
|
||||||
|
this.field_filters["discount"].push(filter_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.field_filters["discount"].length === 0) {
|
||||||
|
delete this.field_filters["discount"];
|
||||||
|
}
|
||||||
|
|
||||||
|
me.change_route_with_filters();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bind_filters() {
|
||||||
|
let me = this;
|
||||||
|
this.field_filters = {};
|
||||||
|
this.attribute_filters = {};
|
||||||
|
|
||||||
|
$('.product-filter').on('change', (e) => {
|
||||||
|
me.from_filters = true;
|
||||||
|
|
||||||
|
const $checkbox = $(e.target);
|
||||||
|
const is_checked = $checkbox.is(':checked');
|
||||||
|
|
||||||
|
if ($checkbox.is('.attribute-filter')) {
|
||||||
|
const {
|
||||||
|
attributeName: attribute_name,
|
||||||
|
attributeValue: attribute_value
|
||||||
|
} = $checkbox.data();
|
||||||
|
|
||||||
|
if (is_checked) {
|
||||||
|
this.attribute_filters[attribute_name] = this.attribute_filters[attribute_name] || [];
|
||||||
|
this.attribute_filters[attribute_name].push(attribute_value);
|
||||||
|
} else {
|
||||||
|
this.attribute_filters[attribute_name] = this.attribute_filters[attribute_name] || [];
|
||||||
|
this.attribute_filters[attribute_name] = this.attribute_filters[attribute_name].filter(v => v !== attribute_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.attribute_filters[attribute_name].length === 0) {
|
||||||
|
delete this.attribute_filters[attribute_name];
|
||||||
|
}
|
||||||
|
} else if ($checkbox.is('.field-filter') || $checkbox.is('.discount-filter')) {
|
||||||
|
const {
|
||||||
|
filterName: filter_name,
|
||||||
|
filterValue: filter_value
|
||||||
|
} = $checkbox.data();
|
||||||
|
|
||||||
|
if ($checkbox.is('.discount-filter')) {
|
||||||
|
// clear previous discount filter to accomodate new
|
||||||
|
delete this.field_filters["discount"];
|
||||||
|
}
|
||||||
|
if (is_checked) {
|
||||||
|
this.field_filters[filter_name] = this.field_filters[filter_name] || [];
|
||||||
|
if (!in_list(this.field_filters[filter_name], filter_value)) {
|
||||||
|
this.field_filters[filter_name].push(filter_value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.field_filters[filter_name] = this.field_filters[filter_name] || [];
|
||||||
|
this.field_filters[filter_name] = this.field_filters[filter_name].filter(v => v !== filter_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.field_filters[filter_name].length === 0) {
|
||||||
|
delete this.field_filters[filter_name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
me.change_route_with_filters();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
change_route_with_filters() {
|
||||||
|
let route_params = frappe.utils.get_query_params();
|
||||||
|
|
||||||
|
let start = this.if_key_exists(route_params.start) || 0;
|
||||||
|
if (this.from_filters) {
|
||||||
|
start = 0; // show items from first page if new filters are triggered
|
||||||
|
}
|
||||||
|
|
||||||
|
const query_string = this.get_query_string({
|
||||||
|
start: start,
|
||||||
|
field_filters: JSON.stringify(this.if_key_exists(this.field_filters)),
|
||||||
|
attribute_filters: JSON.stringify(this.if_key_exists(this.attribute_filters)),
|
||||||
|
});
|
||||||
|
window.history.pushState('filters', '', `${location.pathname}?` + query_string);
|
||||||
|
|
||||||
|
$('.page_content input').prop('disabled', true);
|
||||||
|
|
||||||
|
this.make(true);
|
||||||
|
$('.page_content input').prop('disabled', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_filters_state() {
|
||||||
|
const filters = frappe.utils.get_query_params();
|
||||||
|
let {field_filters, attribute_filters} = filters;
|
||||||
|
|
||||||
|
if (field_filters) {
|
||||||
|
field_filters = JSON.parse(field_filters);
|
||||||
|
for (let fieldname in field_filters) {
|
||||||
|
const values = field_filters[fieldname];
|
||||||
|
const selector = values.map(value => {
|
||||||
|
return `input[data-filter-name="${fieldname}"][data-filter-value="${value}"]`;
|
||||||
|
}).join(',');
|
||||||
|
$(selector).prop('checked', true);
|
||||||
|
}
|
||||||
|
this.field_filters = field_filters;
|
||||||
|
}
|
||||||
|
if (attribute_filters) {
|
||||||
|
attribute_filters = JSON.parse(attribute_filters);
|
||||||
|
for (let attribute in attribute_filters) {
|
||||||
|
const values = attribute_filters[attribute];
|
||||||
|
const selector = values.map(value => {
|
||||||
|
return `input[data-attribute-name="${attribute}"][data-attribute-value="${value}"]`;
|
||||||
|
}).join(',');
|
||||||
|
$(selector).prop('checked', true);
|
||||||
|
}
|
||||||
|
this.attribute_filters = attribute_filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_no_products_section(error=false) {
|
||||||
|
let error_section = `
|
||||||
|
<div class="mt-4 w-100 alert alert-error font-md">
|
||||||
|
Something went wrong. Please refresh or contact us.
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
let no_results_section = `
|
||||||
|
<div class="cart-empty frappe-card mt-4">
|
||||||
|
<div class="cart-empty-state">
|
||||||
|
<img src="/assets/erpnext/images/ui-states/cart-empty-state.png" alt="Empty Cart">
|
||||||
|
</div>
|
||||||
|
<div class="cart-empty-message mt-4">${ __('No products found') }</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.products_section.append(error ? error_section : no_results_section);
|
||||||
|
}
|
||||||
|
|
||||||
|
render_item_sub_categories(categories) {
|
||||||
|
if (categories && categories.length) {
|
||||||
|
let sub_group_html = `
|
||||||
|
<div class="sub-category-container scroll-categories">
|
||||||
|
`;
|
||||||
|
|
||||||
|
categories.forEach(category => {
|
||||||
|
sub_group_html += `
|
||||||
|
<a href="${ category.route || '#' }" style="text-decoration: none;">
|
||||||
|
<div class="category-pill">
|
||||||
|
${ category.name }
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
sub_group_html += `</div>`;
|
||||||
|
|
||||||
|
$("#product-listing").prepend(sub_group_html);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get_query_string(object) {
|
||||||
|
const url = new URLSearchParams();
|
||||||
|
for (let key in object) {
|
||||||
|
const value = object[key];
|
||||||
|
if (value) {
|
||||||
|
url.append(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if_key_exists(obj) {
|
||||||
|
let exists = false;
|
||||||
|
for (let key in obj) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(obj, key) && obj[key]) {
|
||||||
|
exists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return exists ? obj : undefined;
|
||||||
|
}
|
||||||
|
};
|
210
erpnext/e_commerce/redisearch_utils.py
Normal file
210
erpnext/e_commerce/redisearch_utils.py
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.utils.redis_wrapper import RedisWrapper
|
||||||
|
from redisearch import AutoCompleter, Client, IndexDefinition, Suggestion, TagField, TextField
|
||||||
|
|
||||||
|
WEBSITE_ITEM_INDEX = 'website_items_index'
|
||||||
|
WEBSITE_ITEM_KEY_PREFIX = 'website_item:'
|
||||||
|
WEBSITE_ITEM_NAME_AUTOCOMPLETE = 'website_items_name_dict'
|
||||||
|
WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE = 'website_items_category_dict'
|
||||||
|
|
||||||
|
def get_indexable_web_fields():
|
||||||
|
"Return valid fields from Website Item that can be searched for."
|
||||||
|
web_item_meta = frappe.get_meta("Website Item", cached=True)
|
||||||
|
valid_fields = filter(
|
||||||
|
lambda df: df.fieldtype in ("Link", "Table MultiSelect", "Data", "Small Text", "Text Editor"),
|
||||||
|
web_item_meta.fields)
|
||||||
|
|
||||||
|
return [df.fieldname for df in valid_fields]
|
||||||
|
|
||||||
|
def is_search_module_loaded():
|
||||||
|
try:
|
||||||
|
cache = frappe.cache()
|
||||||
|
out = cache.execute_command('MODULE LIST')
|
||||||
|
|
||||||
|
parsed_output = " ".join(
|
||||||
|
(" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out)
|
||||||
|
)
|
||||||
|
return "search" in parsed_output
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def if_redisearch_loaded(function):
|
||||||
|
"Decorator to check if Redisearch is loaded."
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
if is_search_module_loaded():
|
||||||
|
func = function(*args, **kwargs)
|
||||||
|
return func
|
||||||
|
return
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
def make_key(key):
|
||||||
|
return "{0}|{1}".format(frappe.conf.db_name, key).encode('utf-8')
|
||||||
|
|
||||||
|
@if_redisearch_loaded
|
||||||
|
def create_website_items_index():
|
||||||
|
"Creates Index Definition."
|
||||||
|
|
||||||
|
# CREATE index
|
||||||
|
client = Client(make_key(WEBSITE_ITEM_INDEX), conn=frappe.cache())
|
||||||
|
|
||||||
|
# DROP if already exists
|
||||||
|
try:
|
||||||
|
client.drop_index()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
idx_def = IndexDefinition([make_key(WEBSITE_ITEM_KEY_PREFIX)])
|
||||||
|
|
||||||
|
# Based on e-commerce settings
|
||||||
|
idx_fields = frappe.db.get_single_value(
|
||||||
|
'E Commerce Settings',
|
||||||
|
'search_index_fields'
|
||||||
|
)
|
||||||
|
idx_fields = idx_fields.split(',') if idx_fields else []
|
||||||
|
|
||||||
|
if 'web_item_name' in idx_fields:
|
||||||
|
idx_fields.remove('web_item_name')
|
||||||
|
|
||||||
|
idx_fields = list(map(to_search_field, idx_fields))
|
||||||
|
|
||||||
|
client.create_index(
|
||||||
|
[TextField("web_item_name", sortable=True)] + idx_fields,
|
||||||
|
definition=idx_def,
|
||||||
|
)
|
||||||
|
|
||||||
|
reindex_all_web_items()
|
||||||
|
define_autocomplete_dictionary()
|
||||||
|
|
||||||
|
def to_search_field(field):
|
||||||
|
if field == "tags":
|
||||||
|
return TagField("tags", separator=",")
|
||||||
|
|
||||||
|
return TextField(field)
|
||||||
|
|
||||||
|
@if_redisearch_loaded
|
||||||
|
def insert_item_to_index(website_item_doc):
|
||||||
|
# Insert item to index
|
||||||
|
key = get_cache_key(website_item_doc.name)
|
||||||
|
cache = frappe.cache()
|
||||||
|
web_item = create_web_item_map(website_item_doc)
|
||||||
|
|
||||||
|
for k, v in web_item.items():
|
||||||
|
super(RedisWrapper, cache).hset(make_key(key), k, v)
|
||||||
|
|
||||||
|
insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name)
|
||||||
|
|
||||||
|
@if_redisearch_loaded
|
||||||
|
def insert_to_name_ac(web_name, doc_name):
|
||||||
|
ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=frappe.cache())
|
||||||
|
ac.add_suggestions(Suggestion(web_name, payload=doc_name))
|
||||||
|
|
||||||
|
def create_web_item_map(website_item_doc):
|
||||||
|
fields_to_index = get_fields_indexed()
|
||||||
|
web_item = {}
|
||||||
|
|
||||||
|
for f in fields_to_index:
|
||||||
|
web_item[f] = website_item_doc.get(f) or ''
|
||||||
|
|
||||||
|
return web_item
|
||||||
|
|
||||||
|
@if_redisearch_loaded
|
||||||
|
def update_index_for_item(website_item_doc):
|
||||||
|
# Reinsert to Cache
|
||||||
|
insert_item_to_index(website_item_doc)
|
||||||
|
define_autocomplete_dictionary()
|
||||||
|
|
||||||
|
@if_redisearch_loaded
|
||||||
|
def delete_item_from_index(website_item_doc):
|
||||||
|
cache = frappe.cache()
|
||||||
|
key = get_cache_key(website_item_doc.name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cache.delete(key)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
delete_from_ac_dict(website_item_doc)
|
||||||
|
return True
|
||||||
|
|
||||||
|
@if_redisearch_loaded
|
||||||
|
def delete_from_ac_dict(website_item_doc):
|
||||||
|
'''Removes this items's name from autocomplete dictionary'''
|
||||||
|
cache = frappe.cache()
|
||||||
|
name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
|
||||||
|
name_ac.delete(website_item_doc.web_item_name)
|
||||||
|
|
||||||
|
@if_redisearch_loaded
|
||||||
|
def define_autocomplete_dictionary():
|
||||||
|
"""Creates an autocomplete search dictionary for `name`.
|
||||||
|
Also creats autocomplete dictionary for `categories` if
|
||||||
|
checked in E Commerce Settings"""
|
||||||
|
|
||||||
|
cache = frappe.cache()
|
||||||
|
name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
|
||||||
|
cat_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=cache)
|
||||||
|
|
||||||
|
ac_categories = frappe.db.get_single_value(
|
||||||
|
'E Commerce Settings',
|
||||||
|
'show_categories_in_search_autocomplete'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete both autocomplete dicts
|
||||||
|
try:
|
||||||
|
cache.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE))
|
||||||
|
cache.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE))
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
items = frappe.get_all(
|
||||||
|
'Website Item',
|
||||||
|
fields=['web_item_name', 'item_group'],
|
||||||
|
filters={"published": 1}
|
||||||
|
)
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
name_ac.add_suggestions(Suggestion(item.web_item_name))
|
||||||
|
if ac_categories and item.item_group:
|
||||||
|
cat_ac.add_suggestions(Suggestion(item.item_group))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@if_redisearch_loaded
|
||||||
|
def reindex_all_web_items():
|
||||||
|
items = frappe.get_all(
|
||||||
|
'Website Item',
|
||||||
|
fields=get_fields_indexed(),
|
||||||
|
filters={"published": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
cache = frappe.cache()
|
||||||
|
for item in items:
|
||||||
|
web_item = create_web_item_map(item)
|
||||||
|
key = make_key(get_cache_key(item.name))
|
||||||
|
|
||||||
|
for k, v in web_item.items():
|
||||||
|
super(RedisWrapper, cache).hset(key, k, v)
|
||||||
|
|
||||||
|
def get_cache_key(name):
|
||||||
|
name = frappe.scrub(name)
|
||||||
|
return f"{WEBSITE_ITEM_KEY_PREFIX}{name}"
|
||||||
|
|
||||||
|
def get_fields_indexed():
|
||||||
|
fields_to_index = frappe.db.get_single_value(
|
||||||
|
'E Commerce Settings',
|
||||||
|
'search_index_fields'
|
||||||
|
)
|
||||||
|
fields_to_index = fields_to_index.split(',') if fields_to_index else []
|
||||||
|
|
||||||
|
mandatory_fields = ['name', 'web_item_name', 'route', 'thumbnail', 'ranking']
|
||||||
|
fields_to_index = fields_to_index + mandatory_fields
|
||||||
|
|
||||||
|
return fields_to_index
|
||||||
|
|
||||||
|
# TODO: Remove later
|
||||||
|
# # Figure out a way to run this at startup
|
||||||
|
define_autocomplete_dictionary()
|
||||||
|
create_website_items_index()
|
0
erpnext/e_commerce/shopping_cart/__init__.py
Normal file
0
erpnext/e_commerce/shopping_cart/__init__.py
Normal file
@ -1,7 +1,6 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
import frappe.defaults
|
import frappe.defaults
|
||||||
from frappe import _, throw
|
from frappe import _, throw
|
||||||
@ -11,20 +10,20 @@ from frappe.utils import cint, cstr, flt, get_fullname
|
|||||||
from frappe.utils.nestedset import get_root_of
|
from frappe.utils.nestedset import get_root_of
|
||||||
|
|
||||||
from erpnext.accounts.utils import get_account_name
|
from erpnext.accounts.utils import get_account_name
|
||||||
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
|
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||||
get_shopping_cart_settings,
|
get_shopping_cart_settings,
|
||||||
)
|
)
|
||||||
from erpnext.utilities.product import get_qty_in_stock
|
from erpnext.utilities.product import get_web_item_qty_in_stock
|
||||||
|
|
||||||
|
|
||||||
class WebsitePriceListMissingError(frappe.ValidationError):
|
class WebsitePriceListMissingError(frappe.ValidationError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def set_cart_count(quotation=None):
|
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:
|
if not quotation:
|
||||||
quotation = _get_cart_quotation()
|
quotation = _get_cart_quotation()
|
||||||
cart_count = cstr(len(quotation.get("items")))
|
cart_count = cstr(cint(quotation.get("total_qty")))
|
||||||
|
|
||||||
if hasattr(frappe.local, "cookie_manager"):
|
if hasattr(frappe.local, "cookie_manager"):
|
||||||
frappe.local.cookie_manager.set_cookie("cart_count", cart_count)
|
frappe.local.cookie_manager.set_cookie("cart_count", cart_count)
|
||||||
@ -48,7 +47,7 @@ def get_cart_quotation(doc=None):
|
|||||||
"shipping_addresses": get_shipping_addresses(party),
|
"shipping_addresses": get_shipping_addresses(party),
|
||||||
"billing_addresses": get_billing_addresses(party),
|
"billing_addresses": get_billing_addresses(party),
|
||||||
"shipping_rules": get_applicable_shipping_rules(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()
|
@frappe.whitelist()
|
||||||
@ -72,7 +71,7 @@ def get_billing_addresses(party=None):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def place_order():
|
def place_order():
|
||||||
quotation = _get_cart_quotation()
|
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)
|
["company", "allow_items_not_in_stock"], as_dict=1)
|
||||||
quotation.company = cart_settings.company
|
quotation.company = cart_settings.company
|
||||||
|
|
||||||
@ -92,13 +91,19 @@ def place_order():
|
|||||||
|
|
||||||
if not cint(cart_settings.allow_items_not_in_stock):
|
if not cint(cart_settings.allow_items_not_in_stock):
|
||||||
for item in sales_order.get("items"):
|
for item in sales_order.get("items"):
|
||||||
item.reserved_warehouse, is_stock_item = frappe.db.get_value("Item",
|
item.warehouse = frappe.db.get_value(
|
||||||
item.item_code, ["website_warehouse", "is_stock_item"])
|
"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:
|
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):
|
if not cint(item_stock.in_stock):
|
||||||
throw(_("{1} Not in Stock").format(item.item_code))
|
throw(_("{0} Not in Stock").format(item.item_code))
|
||||||
if item.qty > item_stock.stock_qty[0][0]:
|
if item.qty > item_stock.stock_qty[0][0]:
|
||||||
throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code))
|
throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code))
|
||||||
|
|
||||||
@ -156,19 +161,19 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False):
|
|||||||
|
|
||||||
set_cart_count(quotation)
|
set_cart_count(quotation)
|
||||||
|
|
||||||
context = get_cart_quotation(quotation)
|
|
||||||
|
|
||||||
if cint(with_items):
|
if cint(with_items):
|
||||||
|
context = get_cart_quotation(quotation)
|
||||||
return {
|
return {
|
||||||
"items": frappe.render_template("templates/includes/cart/cart_items.html",
|
"items": frappe.render_template("templates/includes/cart/cart_items.html",
|
||||||
context),
|
context),
|
||||||
"taxes": frappe.render_template("templates/includes/order/order_taxes.html",
|
"total": frappe.render_template("templates/includes/cart/cart_items_total.html",
|
||||||
context),
|
context),
|
||||||
|
"taxes_and_totals": frappe.render_template("templates/includes/cart/cart_payment_summary.html",
|
||||||
|
context)
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
'name': quotation.name,
|
'name': quotation.name
|
||||||
'shopping_cart_menu': get_shopping_cart_menu(context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@ -265,13 +270,36 @@ def guess_territory():
|
|||||||
territory = frappe.db.get_value("Territory", geoip_country)
|
territory = frappe.db.get_value("Territory", geoip_country)
|
||||||
|
|
||||||
return territory or \
|
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")
|
get_root_of("Territory")
|
||||||
|
|
||||||
def decorate_quotation_doc(doc):
|
def decorate_quotation_doc(doc):
|
||||||
for d in doc.get("items", []):
|
for d in doc.get("items", []):
|
||||||
d.update(frappe.db.get_value("Item", d.item_code,
|
item_code = d.item_code
|
||||||
["thumbnail", "website_image", "description", "route"], as_dict=True))
|
fields = ["web_item_name", "thumbnail", "website_image", "description", "route"]
|
||||||
|
|
||||||
|
# Variant Item
|
||||||
|
if not frappe.db.exists("Website Item", {"item_code": item_code}):
|
||||||
|
variant_data = frappe.db.get_values(
|
||||||
|
"Item",
|
||||||
|
filters={"item_code": item_code},
|
||||||
|
fieldname=["variant_of", "item_name", "image"],
|
||||||
|
as_dict=True
|
||||||
|
)[0]
|
||||||
|
item_code = variant_data.variant_of
|
||||||
|
fields = fields[1:]
|
||||||
|
d.web_item_name = variant_data.item_name
|
||||||
|
|
||||||
|
if variant_data.image: # get image from variant or template web item
|
||||||
|
d.thumbnail = variant_data.image
|
||||||
|
fields = fields[2:]
|
||||||
|
|
||||||
|
d.update(frappe.db.get_value(
|
||||||
|
"Website Item",
|
||||||
|
{"item_code": item_code},
|
||||||
|
fields,
|
||||||
|
as_dict=True)
|
||||||
|
)
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
@ -288,7 +316,7 @@ def _get_cart_quotation(party=None):
|
|||||||
if quotation:
|
if quotation:
|
||||||
qdoc = frappe.get_doc("Quotation", quotation[0].name)
|
qdoc = frappe.get_doc("Quotation", quotation[0].name)
|
||||||
else:
|
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({
|
qdoc = frappe.get_doc({
|
||||||
"doctype": "Quotation",
|
"doctype": "Quotation",
|
||||||
"naming_series": get_shopping_cart_settings().quotation_series or "QTN-CART-",
|
"naming_series": get_shopping_cart_settings().quotation_series or "QTN-CART-",
|
||||||
@ -343,7 +371,7 @@ def apply_cart_settings(party=None, quotation=None):
|
|||||||
if not quotation:
|
if not quotation:
|
||||||
quotation = _get_cart_quotation(party)
|
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)
|
set_price_list_and_rate(quotation, cart_settings)
|
||||||
|
|
||||||
@ -420,7 +448,7 @@ def get_party(user=None):
|
|||||||
party_doctype = contact.links[0].link_doctype
|
party_doctype = contact.links[0].link_doctype
|
||||||
party = contact.links[0].link_name
|
party = contact.links[0].link_name
|
||||||
|
|
||||||
cart_settings = frappe.get_doc("Shopping Cart Settings")
|
cart_settings = frappe.get_doc("E Commerce Settings")
|
||||||
|
|
||||||
debtors_account = ''
|
debtors_account = ''
|
||||||
|
|
||||||
@ -557,10 +585,20 @@ def get_shipping_rules(quotation=None, cart_settings=None):
|
|||||||
if quotation.shipping_address_name:
|
if quotation.shipping_address_name:
|
||||||
country = frappe.db.get_value("Address", quotation.shipping_address_name, "country")
|
country = frappe.db.get_value("Address", quotation.shipping_address_name, "country")
|
||||||
if country:
|
if country:
|
||||||
shipping_rules = frappe.db.sql_list("""select distinct sr.name
|
sr_country = frappe.qb.DocType("Shipping Rule Country")
|
||||||
from `tabShipping Rule Country` src, `tabShipping Rule` sr
|
sr = frappe.qb.DocType("Shipping Rule")
|
||||||
where src.country = %s and
|
query = (
|
||||||
sr.disabled != 1 and sr.name = src.parent""", country)
|
frappe.qb.from_(sr_country)
|
||||||
|
.join(sr).on(sr.name == sr_country.parent)
|
||||||
|
.select(sr.name)
|
||||||
|
.distinct()
|
||||||
|
.where(
|
||||||
|
(sr_country.country == country)
|
||||||
|
& (sr.disabled != 1)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result = query.run(as_list=True)
|
||||||
|
shipping_rules = [x[0] for x in result]
|
||||||
|
|
||||||
return shipping_rules
|
return shipping_rules
|
||||||
|
|
@ -1,15 +1,18 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from erpnext.shopping_cart.cart import _get_cart_quotation, _set_price_list
|
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||||
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
|
|
||||||
get_shopping_cart_settings,
|
get_shopping_cart_settings,
|
||||||
show_quantity_in_website,
|
show_quantity_in_website,
|
||||||
)
|
)
|
||||||
from erpnext.utilities.product import get_non_stock_item_status, get_price, get_qty_in_stock
|
from erpnext.e_commerce.shopping_cart.cart import _get_cart_quotation, _set_price_list
|
||||||
|
from erpnext.utilities.product import (
|
||||||
|
get_non_stock_item_status,
|
||||||
|
get_price,
|
||||||
|
get_web_item_qty_in_stock,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
@ -18,7 +21,11 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False):
|
|||||||
|
|
||||||
cart_settings = get_shopping_cart_settings()
|
cart_settings = get_shopping_cart_settings()
|
||||||
if not cart_settings.enabled:
|
if not cart_settings.enabled:
|
||||||
return frappe._dict()
|
# return settings even if cart is disabled
|
||||||
|
return frappe._dict({
|
||||||
|
"product_info": {},
|
||||||
|
"cart_settings": cart_settings
|
||||||
|
})
|
||||||
|
|
||||||
cart_quotation = frappe._dict()
|
cart_quotation = frappe._dict()
|
||||||
if not skip_quotation_creation:
|
if not skip_quotation_creation:
|
||||||
@ -26,25 +33,43 @@ 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)
|
selling_price_list = cart_quotation.get("selling_price_list") if cart_quotation else _set_price_list(cart_settings, None)
|
||||||
|
|
||||||
price = get_price(
|
price = {}
|
||||||
item_code,
|
if cart_settings.show_price:
|
||||||
selling_price_list,
|
is_guest = frappe.session.user == "Guest"
|
||||||
cart_settings.default_customer_group,
|
# Show Price if logged in.
|
||||||
cart_settings.company
|
# If not logged in, check if price is hidden for guest.
|
||||||
)
|
if not is_guest or not cart_settings.hide_price_for_guest:
|
||||||
|
price = get_price(
|
||||||
|
item_code,
|
||||||
|
selling_price_list,
|
||||||
|
cart_settings.default_customer_group,
|
||||||
|
cart_settings.company
|
||||||
|
)
|
||||||
|
|
||||||
stock_status = 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 = {
|
product_info = {
|
||||||
"price": price,
|
"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,
|
"qty": 0,
|
||||||
"uom": frappe.db.get_value("Item", item_code, "stock_uom"),
|
"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")
|
"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 product_info["price"]:
|
||||||
if frappe.session.user != "Guest":
|
if frappe.session.user != "Guest":
|
||||||
item = cart_quotation.get({"item_code": item_code}) if cart_quotation else None
|
item = cart_quotation.get({"item_code": item_code}) if cart_quotation else None
|
@ -8,8 +8,14 @@ import frappe
|
|||||||
from frappe.utils import add_months, nowdate
|
from frappe.utils import add_months, nowdate
|
||||||
|
|
||||||
from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule
|
from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule
|
||||||
from erpnext.shopping_cart.cart import _get_cart_quotation, get_party, update_cart
|
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||||
from erpnext.tests.utils import create_test_contact_and_address
|
from erpnext.e_commerce.shopping_cart.cart import (
|
||||||
|
_get_cart_quotation,
|
||||||
|
get_cart_quotation,
|
||||||
|
get_party,
|
||||||
|
update_cart,
|
||||||
|
)
|
||||||
|
from erpnext.tests.utils import change_settings, create_test_contact_and_address
|
||||||
|
|
||||||
# test_dependencies = ['Payment Terms Template']
|
# test_dependencies = ['Payment Terms Template']
|
||||||
|
|
||||||
@ -27,8 +33,14 @@ class TestShoppingCart(unittest.TestCase):
|
|||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
create_test_contact_and_address()
|
create_test_contact_and_address()
|
||||||
self.enable_shopping_cart()
|
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):
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
self.disable_shopping_cart()
|
self.disable_shopping_cart()
|
||||||
|
|
||||||
@ -123,6 +135,43 @@ class TestShoppingCart(unittest.TestCase):
|
|||||||
|
|
||||||
self.remove_test_quotation(quotation)
|
self.remove_test_quotation(quotation)
|
||||||
|
|
||||||
|
@change_settings("E Commerce Settings",{
|
||||||
|
"company": "_Test Company",
|
||||||
|
"enabled": 1,
|
||||||
|
"default_customer_group": "_Test Customer Group",
|
||||||
|
"price_list": "_Test Price List India",
|
||||||
|
"show_price": 1
|
||||||
|
})
|
||||||
|
def test_add_item_variant_without_web_item_to_cart(self):
|
||||||
|
"Test adding Variants having no Website Items in cart via Template Web Item."
|
||||||
|
from erpnext.controllers.item_variant import create_variant
|
||||||
|
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
|
template_item = make_item("Test-Tshirt-Temp", {
|
||||||
|
"has_variant": 1,
|
||||||
|
"variant_based_on": "Item Attribute",
|
||||||
|
"attributes": [
|
||||||
|
{"attribute": "Test Size"},
|
||||||
|
{"attribute": "Test Colour"}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
variant = create_variant("Test-Tshirt-Temp", {
|
||||||
|
"Test Size": "Small", "Test Colour": "Red"
|
||||||
|
})
|
||||||
|
variant.save()
|
||||||
|
make_website_item(template_item) # publish template not variant
|
||||||
|
|
||||||
|
update_cart("Test-Tshirt-Temp-S-R", 1)
|
||||||
|
|
||||||
|
cart = get_cart_quotation() # test if cart page gets data without errors
|
||||||
|
doc = cart.get("doc")
|
||||||
|
|
||||||
|
self.assertEqual(doc.get("items")[0].item_name, "Test-Tshirt-Temp-S-R")
|
||||||
|
|
||||||
|
# test if items are rendered without error
|
||||||
|
frappe.render_template("templates/includes/cart/cart_items.html", cart)
|
||||||
|
|
||||||
def create_tax_rule(self):
|
def create_tax_rule(self):
|
||||||
tax_rule = frappe.get_test_records("Tax Rule")[0]
|
tax_rule = frappe.get_test_records("Tax Rule")[0]
|
||||||
try:
|
try:
|
||||||
@ -166,7 +215,7 @@ class TestShoppingCart(unittest.TestCase):
|
|||||||
|
|
||||||
# helper functions
|
# helper functions
|
||||||
def enable_shopping_cart(self):
|
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({
|
settings.update({
|
||||||
"enabled": 1,
|
"enabled": 1,
|
||||||
@ -196,7 +245,7 @@ class TestShoppingCart(unittest.TestCase):
|
|||||||
frappe.local.shopping_cart_settings = None
|
frappe.local.shopping_cart_settings = None
|
||||||
|
|
||||||
def disable_shopping_cart(self):
|
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.enabled = 0
|
||||||
settings.save()
|
settings.save()
|
||||||
frappe.local.shopping_cart_settings = None
|
frappe.local.shopping_cart_settings = None
|
@ -1,10 +1,8 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
|
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import is_cart_enabled
|
||||||
is_cart_enabled,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def show_cart_count():
|
def show_cart_count():
|
||||||
@ -23,7 +21,7 @@ def set_cart_count(login_manager):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if show_cart_count():
|
if show_cart_count():
|
||||||
from erpnext.shopping_cart.cart import set_cart_count
|
from erpnext.e_commerce.shopping_cart.cart import set_cart_count
|
||||||
|
|
||||||
# set_cart_count will try to fetch existing cart quotation
|
# set_cart_count will try to fetch existing cart quotation
|
||||||
# or create one if non existent (and create a customer too)
|
# or create one if non existent (and create a customer too)
|
0
erpnext/e_commerce/variant_selector/__init__.py
Normal file
0
erpnext/e_commerce/variant_selector/__init__.py
Normal file
@ -44,7 +44,7 @@ class ItemVariantsCacheManager:
|
|||||||
val = frappe.cache().get_value('ordered_attribute_values_map')
|
val = frappe.cache().get_value('ordered_attribute_values_map')
|
||||||
if val: return val
|
if val: return val
|
||||||
|
|
||||||
all_attribute_values = frappe.db.get_all('Item Attribute Value',
|
all_attribute_values = frappe.get_all('Item Attribute Value',
|
||||||
['attribute_value', 'idx', 'parent'], order_by='idx asc')
|
['attribute_value', 'idx', 'parent'], order_by='idx asc')
|
||||||
|
|
||||||
ordered_attribute_values_map = frappe._dict({})
|
ordered_attribute_values_map = frappe._dict({})
|
||||||
@ -57,22 +57,35 @@ class ItemVariantsCacheManager:
|
|||||||
def build_cache(self):
|
def build_cache(self):
|
||||||
parent_item_code = self.item_code
|
parent_item_code = self.item_code
|
||||||
|
|
||||||
attributes = [a.attribute for a in frappe.db.get_all('Item Variant Attribute',
|
attributes = [
|
||||||
{'parent': parent_item_code}, ['attribute'], order_by='idx asc')
|
a.attribute for a in frappe.get_all(
|
||||||
|
'Item Variant Attribute',
|
||||||
|
{'parent': parent_item_code},
|
||||||
|
['attribute'],
|
||||||
|
order_by='idx asc'
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
item_variants_data = frappe.db.get_all('Item Variant Attribute',
|
# join with Website Item
|
||||||
{'variant_of': parent_item_code}, ['parent', 'attribute', 'attribute_value'],
|
item_variants_data = frappe.get_all(
|
||||||
|
'Item Variant Attribute',
|
||||||
|
{'variant_of': parent_item_code},
|
||||||
|
['parent', 'attribute', 'attribute_value'],
|
||||||
order_by='name',
|
order_by='name',
|
||||||
as_list=1
|
as_list=1
|
||||||
)
|
)
|
||||||
|
|
||||||
disabled_items = set([i.name for i in frappe.db.get_all('Item', {'disabled': 1})])
|
disabled_items = set(
|
||||||
|
[i.name for i in frappe.db.get_all('Item', {'disabled': 1})]
|
||||||
|
)
|
||||||
|
|
||||||
attribute_value_item_map = frappe._dict({})
|
attribute_value_item_map = frappe._dict()
|
||||||
item_attribute_value_map = frappe._dict({})
|
item_attribute_value_map = frappe._dict()
|
||||||
|
|
||||||
|
# dont consider variants that are disabled
|
||||||
|
# pull all other variants
|
||||||
item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items]
|
item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items]
|
||||||
|
|
||||||
for row in item_variants_data:
|
for row in item_variants_data:
|
||||||
item_code, attribute, attribute_value = row
|
item_code, attribute, attribute_value = row
|
||||||
# (attr, value) => [item1, item2]
|
# (attr, value) => [item1, item2]
|
117
erpnext/e_commerce/variant_selector/test_variant_selector.py
Normal file
117
erpnext/e_commerce/variant_selector/test_variant_selector.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.controllers.item_variant import create_variant
|
||||||
|
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
|
||||||
|
setup_e_commerce_settings,
|
||||||
|
)
|
||||||
|
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||||
|
from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
|
test_dependencies = ["Item"]
|
||||||
|
|
||||||
|
class TestVariantSelector(ERPNextTestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
template_item = make_item("Test-Tshirt-Temp", {
|
||||||
|
"has_variant": 1,
|
||||||
|
"variant_based_on": "Item Attribute",
|
||||||
|
"attributes": [
|
||||||
|
{"attribute": "Test Size"},
|
||||||
|
{"attribute": "Test Colour"}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
# create L-R, L-G, M-R, M-G and S-R
|
||||||
|
for size in ("Large", "Medium",):
|
||||||
|
for colour in ("Red", "Green",):
|
||||||
|
variant = create_variant("Test-Tshirt-Temp", {
|
||||||
|
"Test Size": size, "Test Colour": colour
|
||||||
|
})
|
||||||
|
variant.save()
|
||||||
|
|
||||||
|
variant = create_variant("Test-Tshirt-Temp", {
|
||||||
|
"Test Size": "Small", "Test Colour": "Red"
|
||||||
|
})
|
||||||
|
variant.save()
|
||||||
|
|
||||||
|
make_website_item(template_item) # publish template not variants
|
||||||
|
|
||||||
|
def test_item_attributes(self):
|
||||||
|
"""
|
||||||
|
Test if the right attributes are fetched in the popup.
|
||||||
|
(Attributes must only come from active items)
|
||||||
|
|
||||||
|
Attribute selection must not be linked to Website Items.
|
||||||
|
"""
|
||||||
|
from erpnext.e_commerce.variant_selector.utils import get_attributes_and_values
|
||||||
|
|
||||||
|
attr_data = get_attributes_and_values("Test-Tshirt-Temp")
|
||||||
|
|
||||||
|
self.assertEqual(attr_data[0]["attribute"], "Test Size")
|
||||||
|
self.assertEqual(attr_data[1]["attribute"], "Test Colour")
|
||||||
|
self.assertEqual(len(attr_data[0]["values"]), 3) # ['Small', 'Medium', 'Large']
|
||||||
|
self.assertEqual(len(attr_data[1]["values"]), 2) # ['Red', 'Green']
|
||||||
|
|
||||||
|
# disable small red tshirt, now there are no small tshirts.
|
||||||
|
# but there are some red tshirts
|
||||||
|
small_variant = frappe.get_doc("Item", "Test-Tshirt-Temp-S-R")
|
||||||
|
small_variant.disabled = 1
|
||||||
|
small_variant.save() # trigger cache rebuild
|
||||||
|
|
||||||
|
attr_data = get_attributes_and_values("Test-Tshirt-Temp")
|
||||||
|
|
||||||
|
# Only L and M attribute values must be fetched since S is disabled
|
||||||
|
self.assertEqual(len(attr_data[0]["values"]), 2) # ['Medium', 'Large']
|
||||||
|
|
||||||
|
# teardown
|
||||||
|
small_variant.disabled = 0
|
||||||
|
small_variant.save()
|
||||||
|
|
||||||
|
def test_next_item_variant_values(self):
|
||||||
|
"""
|
||||||
|
Test if on selecting an attribute value, the next possible values
|
||||||
|
are filtered accordingly.
|
||||||
|
Values that dont apply should not be fetched.
|
||||||
|
E.g.
|
||||||
|
There is a ** Small-Red ** Tshirt. No other colour in this size.
|
||||||
|
On selecting ** Small **, only ** Red ** should be selectable next.
|
||||||
|
"""
|
||||||
|
next_values = get_next_attribute_and_values("Test-Tshirt-Temp", selected_attributes={"Test Size": "Small"})
|
||||||
|
next_colours = next_values["valid_options_for_attributes"]["Test Colour"]
|
||||||
|
filtered_items = next_values["filtered_items"]
|
||||||
|
|
||||||
|
self.assertEqual(len(next_colours), 1)
|
||||||
|
self.assertEqual(next_colours.pop(), "Red")
|
||||||
|
self.assertEqual(len(filtered_items), 1)
|
||||||
|
self.assertEqual(filtered_items.pop(), "Test-Tshirt-Temp-S-R")
|
||||||
|
|
||||||
|
def test_exact_match_with_price(self):
|
||||||
|
"""
|
||||||
|
Test price fetching and matching of variant without Website Item
|
||||||
|
"""
|
||||||
|
from erpnext.e_commerce.doctype.website_item.test_website_item import make_web_item_price
|
||||||
|
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
setup_e_commerce_settings({
|
||||||
|
"company": "_Test Company",
|
||||||
|
"enabled": 1,
|
||||||
|
"default_customer_group": "_Test Customer Group",
|
||||||
|
"price_list": "_Test Price List India",
|
||||||
|
"show_price": 1
|
||||||
|
})
|
||||||
|
|
||||||
|
make_web_item_price(item_code="Test-Tshirt-Temp-S-R", price_list_rate=100)
|
||||||
|
next_values = get_next_attribute_and_values(
|
||||||
|
"Test-Tshirt-Temp",
|
||||||
|
selected_attributes={"Test Size": "Small", "Test Colour": "Red"}
|
||||||
|
)
|
||||||
|
print(">>>>", next_values)
|
||||||
|
price_info = next_values["product_info"]["price"]
|
||||||
|
|
||||||
|
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
|
||||||
|
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
|
||||||
|
self.assertEqual(price_info["price_list_rate"], 100.0)
|
||||||
|
self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00")
|
218
erpnext/e_commerce/variant_selector/utils.py
Normal file
218
erpnext/e_commerce/variant_selector/utils.py
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.utils import cint
|
||||||
|
|
||||||
|
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||||
|
get_shopping_cart_settings,
|
||||||
|
)
|
||||||
|
from erpnext.e_commerce.shopping_cart.cart import _set_price_list
|
||||||
|
from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager
|
||||||
|
from erpnext.utilities.product import get_price
|
||||||
|
|
||||||
|
|
||||||
|
def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
|
||||||
|
items = []
|
||||||
|
|
||||||
|
for attribute, values in attribute_filters.items():
|
||||||
|
attribute_values = values
|
||||||
|
|
||||||
|
if not isinstance(attribute_values, list):
|
||||||
|
attribute_values = [attribute_values]
|
||||||
|
|
||||||
|
if not attribute_values:
|
||||||
|
continue
|
||||||
|
|
||||||
|
wheres = []
|
||||||
|
query_values = []
|
||||||
|
for attribute_value in attribute_values:
|
||||||
|
wheres.append('( attribute = %s and attribute_value = %s )')
|
||||||
|
query_values += [attribute, attribute_value]
|
||||||
|
|
||||||
|
attribute_query = ' or '.join(wheres)
|
||||||
|
|
||||||
|
if template_item_code:
|
||||||
|
variant_of_query = 'AND t2.variant_of = %s'
|
||||||
|
query_values.append(template_item_code)
|
||||||
|
else:
|
||||||
|
variant_of_query = ''
|
||||||
|
|
||||||
|
query = '''
|
||||||
|
SELECT
|
||||||
|
t1.parent
|
||||||
|
FROM
|
||||||
|
`tabItem Variant Attribute` t1
|
||||||
|
WHERE
|
||||||
|
1 = 1
|
||||||
|
AND (
|
||||||
|
{attribute_query}
|
||||||
|
)
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
`tabItem` t2
|
||||||
|
WHERE
|
||||||
|
t2.name = t1.parent
|
||||||
|
{variant_of_query}
|
||||||
|
)
|
||||||
|
GROUP BY
|
||||||
|
t1.parent
|
||||||
|
ORDER BY
|
||||||
|
NULL
|
||||||
|
'''.format(attribute_query=attribute_query, variant_of_query=variant_of_query)
|
||||||
|
|
||||||
|
item_codes = set([r[0] for r in frappe.db.sql(query, query_values)]) # nosemgrep
|
||||||
|
items.append(item_codes)
|
||||||
|
|
||||||
|
res = list(set.intersection(*items))
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def get_attributes_and_values(item_code):
|
||||||
|
'''Build a list of attributes and their possible values.
|
||||||
|
This will ignore the values upon selection of which there cannot exist one item.
|
||||||
|
'''
|
||||||
|
item_cache = ItemVariantsCacheManager(item_code)
|
||||||
|
item_variants_data = item_cache.get_item_variants_data()
|
||||||
|
|
||||||
|
attributes = get_item_attributes(item_code)
|
||||||
|
attribute_list = [a.attribute for a in attributes]
|
||||||
|
|
||||||
|
valid_options = {}
|
||||||
|
for item_code, attribute, attribute_value in item_variants_data:
|
||||||
|
if attribute in attribute_list:
|
||||||
|
valid_options.setdefault(attribute, set()).add(attribute_value)
|
||||||
|
|
||||||
|
item_attribute_values = frappe.db.get_all('Item Attribute Value',
|
||||||
|
['parent', 'attribute_value', 'idx'], order_by='parent asc, idx asc')
|
||||||
|
ordered_attribute_value_map = frappe._dict()
|
||||||
|
for iv in item_attribute_values:
|
||||||
|
ordered_attribute_value_map.setdefault(iv.parent, []).append(iv.attribute_value)
|
||||||
|
|
||||||
|
# build attribute values in idx order
|
||||||
|
for attr in attributes:
|
||||||
|
valid_attribute_values = valid_options.get(attr.attribute, [])
|
||||||
|
ordered_values = ordered_attribute_value_map.get(attr.attribute, [])
|
||||||
|
attr['values'] = [v for v in ordered_values if v in valid_attribute_values]
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def get_next_attribute_and_values(item_code, selected_attributes):
|
||||||
|
'''Find the count of Items that match the selected attributes.
|
||||||
|
Also, find the attribute values that are not applicable for further searching.
|
||||||
|
If less than equal to 10 items are found, return item_codes of those items.
|
||||||
|
If one item is matched exactly, return item_code of that item.
|
||||||
|
'''
|
||||||
|
selected_attributes = frappe.parse_json(selected_attributes)
|
||||||
|
|
||||||
|
item_cache = ItemVariantsCacheManager(item_code)
|
||||||
|
item_variants_data = item_cache.get_item_variants_data()
|
||||||
|
|
||||||
|
attributes = get_item_attributes(item_code)
|
||||||
|
attribute_list = [a.attribute for a in attributes]
|
||||||
|
filtered_items = get_items_with_selected_attributes(item_code, selected_attributes)
|
||||||
|
|
||||||
|
next_attribute = None
|
||||||
|
|
||||||
|
for attribute in attribute_list:
|
||||||
|
if attribute not in selected_attributes:
|
||||||
|
next_attribute = attribute
|
||||||
|
break
|
||||||
|
|
||||||
|
valid_options_for_attributes = frappe._dict()
|
||||||
|
|
||||||
|
for a in attribute_list:
|
||||||
|
valid_options_for_attributes[a] = set()
|
||||||
|
|
||||||
|
selected_attribute = selected_attributes.get(a, None)
|
||||||
|
if selected_attribute:
|
||||||
|
# already selected attribute values are valid options
|
||||||
|
valid_options_for_attributes[a].add(selected_attribute)
|
||||||
|
|
||||||
|
for row in item_variants_data:
|
||||||
|
item_code, attribute, attribute_value = row
|
||||||
|
if item_code in filtered_items and attribute not in selected_attributes and attribute in attribute_list:
|
||||||
|
valid_options_for_attributes[attribute].add(attribute_value)
|
||||||
|
|
||||||
|
optional_attributes = item_cache.get_optional_attributes()
|
||||||
|
exact_match = []
|
||||||
|
# search for exact match if all selected attributes are required attributes
|
||||||
|
if len(selected_attributes.keys()) >= (len(attribute_list) - len(optional_attributes)):
|
||||||
|
item_attribute_value_map = item_cache.get_item_attribute_value_map()
|
||||||
|
for item_code, attr_dict in item_attribute_value_map.items():
|
||||||
|
if item_code in filtered_items and set(attr_dict.keys()) == set(selected_attributes.keys()):
|
||||||
|
exact_match.append(item_code)
|
||||||
|
|
||||||
|
filtered_items_count = len(filtered_items)
|
||||||
|
|
||||||
|
# get product info if exact match
|
||||||
|
# from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
|
||||||
|
if exact_match:
|
||||||
|
cart_settings = get_shopping_cart_settings()
|
||||||
|
product_info = get_item_variant_price_dict(exact_match[0], cart_settings)
|
||||||
|
|
||||||
|
if product_info:
|
||||||
|
product_info["allow_items_not_in_stock"] = cint(cart_settings.allow_items_not_in_stock)
|
||||||
|
else:
|
||||||
|
product_info = None
|
||||||
|
|
||||||
|
return {
|
||||||
|
'next_attribute': next_attribute,
|
||||||
|
'valid_options_for_attributes': valid_options_for_attributes,
|
||||||
|
'filtered_items_count': filtered_items_count,
|
||||||
|
'filtered_items': filtered_items if filtered_items_count < 10 else [],
|
||||||
|
'exact_match': exact_match,
|
||||||
|
'product_info': product_info
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_items_with_selected_attributes(item_code, selected_attributes):
|
||||||
|
item_cache = ItemVariantsCacheManager(item_code)
|
||||||
|
attribute_value_item_map = item_cache.get_attribute_value_item_map()
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for attribute, value in selected_attributes.items():
|
||||||
|
filtered_items = attribute_value_item_map.get((attribute, value), [])
|
||||||
|
items.append(set(filtered_items))
|
||||||
|
|
||||||
|
return set.intersection(*items)
|
||||||
|
|
||||||
|
# utilities
|
||||||
|
|
||||||
|
def get_item_attributes(item_code):
|
||||||
|
attributes = frappe.db.get_all('Item Variant Attribute',
|
||||||
|
fields=['attribute'],
|
||||||
|
filters={
|
||||||
|
'parenttype': 'Item',
|
||||||
|
'parent': item_code
|
||||||
|
},
|
||||||
|
order_by='idx asc'
|
||||||
|
)
|
||||||
|
|
||||||
|
optional_attributes = ItemVariantsCacheManager(item_code).get_optional_attributes()
|
||||||
|
|
||||||
|
for a in attributes:
|
||||||
|
if a.attribute in optional_attributes:
|
||||||
|
a.optional = True
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
def get_item_variant_price_dict(item_code, cart_settings):
|
||||||
|
if cart_settings.enabled and cart_settings.show_price:
|
||||||
|
is_guest = frappe.session.user == "Guest"
|
||||||
|
# Show Price if logged in.
|
||||||
|
# If not logged in, check if price is hidden for guest.
|
||||||
|
if not is_guest or not cart_settings.hide_price_for_guest:
|
||||||
|
price_list = _set_price_list(cart_settings, None)
|
||||||
|
price = get_price(
|
||||||
|
item_code,
|
||||||
|
price_list,
|
||||||
|
cart_settings.default_customer_group,
|
||||||
|
cart_settings.company
|
||||||
|
)
|
||||||
|
return {"price": price}
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
0
erpnext/e_commerce/web_template/__init__.py
Normal file
0
erpnext/e_commerce/web_template/__init__.py
Normal file
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"__unsaved": 1,
|
||||||
"creation": "2020-11-17 15:21:51.207221",
|
"creation": "2020-11-17 15:21:51.207221",
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Web Template",
|
"doctype": "Web Template",
|
||||||
@ -273,9 +274,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 2,
|
"idx": 2,
|
||||||
"modified": "2020-12-29 12:30:02.794994",
|
"modified": "2021-02-24 15:57:05.889709",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Shopping Cart",
|
"module": "E-commerce",
|
||||||
"name": "Hero Slider",
|
"name": "Hero Slider",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"standard": 1,
|
"standard": 1,
|
@ -23,11 +23,10 @@
|
|||||||
{%- for index in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] -%}
|
{%- for index in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] -%}
|
||||||
{%- set item = values['card_' + index + '_item'] -%}
|
{%- set item = values['card_' + index + '_item'] -%}
|
||||||
{%- if item -%}
|
{%- if item -%}
|
||||||
{%- set item = frappe.get_doc("Item", item) -%}
|
{%- set web_item = frappe.get_doc("Website Item", item) -%}
|
||||||
{{ item_card(
|
{{ item_card(
|
||||||
item.item_name, item.image, item.route, item.description,
|
web_item, is_featured=values['card_' + index + '_featured'],
|
||||||
None, item.item_group, values['card_' + index + '_featured'],
|
is_full_width=True, align="Center"
|
||||||
True, "Center"
|
|
||||||
) }}
|
) }}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
@ -17,15 +17,12 @@
|
|||||||
"reqd": 0
|
"reqd": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"__unsaved": 1,
|
|
||||||
"fieldname": "primary_action_label",
|
"fieldname": "primary_action_label",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Primary Action Label",
|
"label": "Primary Action Label",
|
||||||
"reqd": 0
|
"reqd": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"__islocal": 1,
|
|
||||||
"__unsaved": 1,
|
|
||||||
"fieldname": "primary_action",
|
"fieldname": "primary_action",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Primary Action",
|
"label": "Primary Action",
|
||||||
@ -40,8 +37,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "card_1_item",
|
"fieldname": "card_1_item",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Item",
|
"label": "Website Item",
|
||||||
"options": "Item",
|
"options": "Website Item",
|
||||||
"reqd": 0
|
"reqd": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -59,8 +56,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "card_2_item",
|
"fieldname": "card_2_item",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Item",
|
"label": "Website Item",
|
||||||
"options": "Item",
|
"options": "Website Item",
|
||||||
"reqd": 0
|
"reqd": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -79,8 +76,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "card_3_item",
|
"fieldname": "card_3_item",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Item",
|
"label": "Website Item",
|
||||||
"options": "Item",
|
"options": "Website Item",
|
||||||
"reqd": 0
|
"reqd": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -98,8 +95,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "card_4_item",
|
"fieldname": "card_4_item",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Item",
|
"label": "Website Item",
|
||||||
"options": "Item",
|
"options": "Website Item",
|
||||||
"reqd": 0
|
"reqd": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -117,8 +114,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "card_5_item",
|
"fieldname": "card_5_item",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Item",
|
"label": "Website Item",
|
||||||
"options": "Item",
|
"options": "Website Item",
|
||||||
"reqd": 0
|
"reqd": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -136,8 +133,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "card_6_item",
|
"fieldname": "card_6_item",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Item",
|
"label": "Website Item",
|
||||||
"options": "Item",
|
"options": "Website Item",
|
||||||
"reqd": 0
|
"reqd": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -155,8 +152,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "card_7_item",
|
"fieldname": "card_7_item",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Item",
|
"label": "Website Item",
|
||||||
"options": "Item",
|
"options": "Website Item",
|
||||||
"reqd": 0
|
"reqd": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -174,8 +171,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "card_8_item",
|
"fieldname": "card_8_item",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Item",
|
"label": "Website Item",
|
||||||
"options": "Item",
|
"options": "Website Item",
|
||||||
"reqd": 0
|
"reqd": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -193,8 +190,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "card_9_item",
|
"fieldname": "card_9_item",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Item",
|
"label": "Website Item",
|
||||||
"options": "Item",
|
"options": "Website Item",
|
||||||
"reqd": 0
|
"reqd": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -212,8 +209,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "card_10_item",
|
"fieldname": "card_10_item",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Item",
|
"label": "Website Item",
|
||||||
"options": "Item",
|
"options": "Website Item",
|
||||||
"reqd": 0
|
"reqd": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -231,8 +228,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "card_11_item",
|
"fieldname": "card_11_item",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Item",
|
"label": "Website Item",
|
||||||
"options": "Item",
|
"options": "Website Item",
|
||||||
"reqd": 0
|
"reqd": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -250,8 +247,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "card_12_item",
|
"fieldname": "card_12_item",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Item",
|
"label": "Website Item",
|
||||||
"options": "Item",
|
"options": "Website Item",
|
||||||
"reqd": 0
|
"reqd": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -262,9 +259,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"modified": "2020-11-19 18:48:52.633045",
|
"modified": "2021-12-21 14:44:59.821335",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Shopping Cart",
|
"module": "E-commerce",
|
||||||
"name": "Item Card Group",
|
"name": "Item Card Group",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"standard": 1,
|
"standard": 1,
|
@ -5,7 +5,6 @@
|
|||||||
"doctype": "Web Template",
|
"doctype": "Web Template",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"__unsaved": 1,
|
|
||||||
"fieldname": "item",
|
"fieldname": "item",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Item",
|
"label": "Item",
|
||||||
@ -13,7 +12,6 @@
|
|||||||
"reqd": 0
|
"reqd": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"__unsaved": 1,
|
|
||||||
"fieldname": "featured",
|
"fieldname": "featured",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Featured",
|
"label": "Featured",
|
||||||
@ -22,9 +20,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"modified": "2020-11-17 15:33:34.982515",
|
"modified": "2021-02-24 16:05:17.926610",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Shopping Cart",
|
"module": "E-commerce",
|
||||||
"name": "Product Card",
|
"name": "Product Card",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"standard": 1,
|
"standard": 1,
|
@ -6,8 +6,15 @@
|
|||||||
}) -%}
|
}) -%}
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
{% if image %}
|
{% 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 %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="card-body text-center text-muted small">
|
<div class="card-body text-center text-muted small">
|
||||||
{{ title or '' }}
|
{{ title or '' }}
|
||||||
</div>
|
</div>
|
@ -74,9 +74,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"modified": "2020-11-18 17:26:28.726260",
|
"modified": "2021-02-24 16:03:33.835635",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Shopping Cart",
|
"module": "E-commerce",
|
||||||
"name": "Product Category Cards",
|
"name": "Product Category Cards",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"standard": 1,
|
"standard": 1,
|
@ -149,7 +149,6 @@ def create_item_code(amazon_item_json, sku):
|
|||||||
item.description = amazon_item_json.Product.AttributeSets.ItemAttributes.Title
|
item.description = amazon_item_json.Product.AttributeSets.ItemAttributes.Title
|
||||||
item.brand = new_brand
|
item.brand = new_brand
|
||||||
item.manufacturer = new_manufacturer
|
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
|
item.image = amazon_item_json.Product.AttributeSets.ItemAttributes.SmallImage.URL
|
||||||
|
|
||||||
|
@ -51,15 +51,15 @@ additional_print_settings = "erpnext.controllers.print_settings.get_print_settin
|
|||||||
|
|
||||||
on_session_creation = [
|
on_session_creation = [
|
||||||
"erpnext.portal.utils.create_customer_or_supplier",
|
"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']
|
treeviews = ['Account', 'Cost Center', 'Warehouse', 'Item Group', 'Customer Group', 'Sales Person', 'Territory', 'Assessment Group', 'Department']
|
||||||
|
|
||||||
# website
|
# website
|
||||||
update_website_context = ["erpnext.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_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.shopping_cart.utils.update_my_account_context"
|
my_account_context = "erpnext.e_commerce.shopping_cart.utils.update_my_account_context"
|
||||||
webform_list_context = "erpnext.controllers.website_list_for_contact.get_webform_list_context"
|
webform_list_context = "erpnext.controllers.website_list_for_contact.get_webform_list_context"
|
||||||
|
|
||||||
calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "Course Schedule"]
|
calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "Course Schedule"]
|
||||||
@ -73,7 +73,7 @@ domains = {
|
|||||||
'Services': 'erpnext.domains.services',
|
'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"]
|
"Job Opening", "Student Admission"]
|
||||||
|
|
||||||
website_context = {
|
website_context = {
|
||||||
@ -237,10 +237,7 @@ doc_events = {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Sales Taxes and Charges Template": {
|
"Sales Taxes and Charges Template": {
|
||||||
"on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings"
|
"on_update": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.validate_cart_settings"
|
||||||
},
|
|
||||||
"Website Settings": {
|
|
||||||
"validate": "erpnext.portal.doctype.products_settings.products_settings.home_page_is_products"
|
|
||||||
},
|
},
|
||||||
"Tax Category": {
|
"Tax Category": {
|
||||||
"validate": "erpnext.regional.india.utils.validate_tax_category"
|
"validate": "erpnext.regional.india.utils.validate_tax_category"
|
||||||
|
@ -9,7 +9,6 @@ Manufacturing
|
|||||||
Stock
|
Stock
|
||||||
Support
|
Support
|
||||||
Utilities
|
Utilities
|
||||||
Shopping Cart
|
|
||||||
Assets
|
Assets
|
||||||
Portal
|
Portal
|
||||||
Maintenance
|
Maintenance
|
||||||
@ -22,3 +21,4 @@ Communication
|
|||||||
Loan Management
|
Loan Management
|
||||||
Payroll
|
Payroll
|
||||||
Telephony
|
Telephony
|
||||||
|
E-commerce
|
||||||
|
@ -293,6 +293,9 @@ erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021
|
|||||||
erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
|
erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
|
||||||
erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021
|
erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021
|
||||||
erpnext.patches.v13_0.fix_invoice_statuses
|
erpnext.patches.v13_0.fix_invoice_statuses
|
||||||
|
erpnext.patches.v13_0.create_website_items #30-09-2021
|
||||||
|
erpnext.patches.v13_0.populate_e_commerce_settings
|
||||||
|
erpnext.patches.v13_0.make_homepage_products_website_items
|
||||||
erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item
|
erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item
|
||||||
erpnext.patches.v13_0.update_dates_in_tax_withholding_category
|
erpnext.patches.v13_0.update_dates_in_tax_withholding_category
|
||||||
erpnext.patches.v14_0.update_opportunity_currency_fields
|
erpnext.patches.v14_0.update_opportunity_currency_fields
|
||||||
@ -314,6 +317,7 @@ erpnext.patches.v13_0.item_naming_series_not_mandatory
|
|||||||
erpnext.patches.v14_0.delete_healthcare_doctypes
|
erpnext.patches.v14_0.delete_healthcare_doctypes
|
||||||
erpnext.patches.v13_0.update_category_in_ltds_certificate
|
erpnext.patches.v13_0.update_category_in_ltds_certificate
|
||||||
erpnext.patches.v13_0.create_pan_field_for_india #2
|
erpnext.patches.v13_0.create_pan_field_for_india #2
|
||||||
|
erpnext.patches.v13_0.fetch_thumbnail_in_website_items
|
||||||
erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit
|
erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit
|
||||||
erpnext.patches.v13_0.create_ksa_vat_custom_fields # 07-01-2022
|
erpnext.patches.v13_0.create_ksa_vat_custom_fields # 07-01-2022
|
||||||
erpnext.patches.v14_0.migrate_crm_settings
|
erpnext.patches.v14_0.migrate_crm_settings
|
||||||
@ -343,3 +347,5 @@ erpnext.patches.v14_0.restore_einvoice_fields
|
|||||||
erpnext.patches.v13_0.update_sane_transfer_against
|
erpnext.patches.v13_0.update_sane_transfer_against
|
||||||
erpnext.patches.v12_0.add_company_link_to_einvoice_settings
|
erpnext.patches.v12_0.add_company_link_to_einvoice_settings
|
||||||
erpnext.patches.v14_0.migrate_cost_center_allocations
|
erpnext.patches.v14_0.migrate_cost_center_allocations
|
||||||
|
erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template
|
||||||
|
erpnext.patches.v13_0.shopping_cart_to_ecommerce
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
import json
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
"""
|
||||||
|
Convert all Item links to Website Item link values in
|
||||||
|
exisitng 'Item Card Group' Web Page Block data.
|
||||||
|
"""
|
||||||
|
frappe.reload_doc("e_commerce", "web_template", "item_card_group")
|
||||||
|
|
||||||
|
blocks = frappe.db.get_all(
|
||||||
|
"Web Page Block",
|
||||||
|
filters={"web_template": "Item Card Group"},
|
||||||
|
fields=["parent", "web_template_values", "name"]
|
||||||
|
)
|
||||||
|
|
||||||
|
fields = generate_fields_to_edit()
|
||||||
|
|
||||||
|
for block in blocks:
|
||||||
|
web_template_value = json.loads(block.get('web_template_values'))
|
||||||
|
|
||||||
|
for field in fields:
|
||||||
|
item = web_template_value.get(field)
|
||||||
|
if not item:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if frappe.db.exists("Website Item", {"item_code": item}):
|
||||||
|
website_item = frappe.db.get_value("Website Item", {"item_code": item})
|
||||||
|
else:
|
||||||
|
website_item = make_new_website_item(item)
|
||||||
|
|
||||||
|
if website_item:
|
||||||
|
web_template_value[field] = website_item
|
||||||
|
|
||||||
|
frappe.db.set_value("Web Page Block", block.name, "web_template_values", json.dumps(web_template_value))
|
||||||
|
|
||||||
|
def generate_fields_to_edit() -> List:
|
||||||
|
fields = []
|
||||||
|
for i in range(1, 13):
|
||||||
|
fields.append(f"card_{i}_item") # fields like 'card_1_item', etc.
|
||||||
|
|
||||||
|
return fields
|
||||||
|
|
||||||
|
def make_new_website_item(item: str) -> Union[str, None]:
|
||||||
|
try:
|
||||||
|
doc = frappe.get_doc("Item", item)
|
||||||
|
web_item = make_website_item(doc) # returns [website_item.name, item_name]
|
||||||
|
return web_item[0]
|
||||||
|
except Exception:
|
||||||
|
title = f"{item}: Error while converting to Website Item "
|
||||||
|
frappe.log_error(title + "for Item Card Group Template" + "\n\n" + frappe.get_traceback(), title=title)
|
||||||
|
return None
|
72
erpnext/patches/v13_0/create_website_items.py
Normal file
72
erpnext/patches/v13_0/create_website_items.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc("e_commerce", "doctype", "website_item")
|
||||||
|
frappe.reload_doc("e_commerce", "doctype", "website_item_tabbed_section")
|
||||||
|
frappe.reload_doc("e_commerce", "doctype", "website_offer")
|
||||||
|
frappe.reload_doc("e_commerce", "doctype", "recommended_items")
|
||||||
|
frappe.reload_doc("e_commerce", "doctype", "e_commerce_settings")
|
||||||
|
frappe.reload_doc("stock", "doctype", "item")
|
||||||
|
|
||||||
|
item_fields = ["item_code", "item_name", "item_group", "stock_uom", "brand", "image",
|
||||||
|
"has_variants", "variant_of", "description", "weightage"]
|
||||||
|
web_fields_to_map = ["route", "slideshow", "website_image_alt",
|
||||||
|
"website_warehouse", "web_long_description", "website_content", "thumbnail"]
|
||||||
|
|
||||||
|
# get all valid columns (fields) from Item master DB schema
|
||||||
|
item_table_fields = frappe.db.sql("desc `tabItem`", as_dict=1) # nosemgrep
|
||||||
|
item_table_fields = [d.get('Field') for d in item_table_fields]
|
||||||
|
|
||||||
|
# prepare fields to query from Item, check if the web field exists in Item master
|
||||||
|
web_query_fields = []
|
||||||
|
for web_field in web_fields_to_map:
|
||||||
|
if web_field in item_table_fields:
|
||||||
|
web_query_fields.append(web_field)
|
||||||
|
item_fields.append(web_field)
|
||||||
|
|
||||||
|
# check if the filter fields exist in Item master
|
||||||
|
or_filters = {}
|
||||||
|
for field in ["show_in_website", "show_variant_in_website"]:
|
||||||
|
if field in item_table_fields:
|
||||||
|
or_filters[field] = 1
|
||||||
|
|
||||||
|
if not web_query_fields or not or_filters:
|
||||||
|
# web fields to map are not present in Item master schema
|
||||||
|
# most likely a fresh installation that doesnt need this patch
|
||||||
|
return
|
||||||
|
|
||||||
|
items = frappe.db.get_all(
|
||||||
|
"Item",
|
||||||
|
fields=item_fields,
|
||||||
|
or_filters=or_filters
|
||||||
|
)
|
||||||
|
total_count = len(items)
|
||||||
|
|
||||||
|
for count, item in enumerate(items, start=1):
|
||||||
|
if frappe.db.exists("Website Item", {"item_code": item.item_code}):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# make new website item from item (publish item)
|
||||||
|
website_item = make_website_item(item, save=False)
|
||||||
|
website_item.ranking = item.get("weightage")
|
||||||
|
|
||||||
|
for field in web_fields_to_map:
|
||||||
|
website_item.update({field: item.get(field)})
|
||||||
|
|
||||||
|
website_item.save()
|
||||||
|
|
||||||
|
# move Website Item Group & Website Specification table to Website Item
|
||||||
|
for doctype in ("Website Item Group", "Item Website Specification"):
|
||||||
|
frappe.db.set_value(
|
||||||
|
doctype,
|
||||||
|
{"parenttype": "Item", "parent": item.item_code}, # filters
|
||||||
|
{"parenttype": "Website Item", "parent": website_item.name} # value dict
|
||||||
|
)
|
||||||
|
|
||||||
|
if count % 20 == 0: # commit after every 20 items
|
||||||
|
frappe.db.commit()
|
||||||
|
|
||||||
|
frappe.utils.update_progress_bar('Creating Website Items', count, total_count)
|
16
erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py
Normal file
16
erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
if frappe.db.has_column("Item", "thumbnail"):
|
||||||
|
website_item = frappe.qb.DocType("Website Item").as_("wi")
|
||||||
|
item = frappe.qb.DocType("Item")
|
||||||
|
|
||||||
|
frappe.qb.update(website_item).inner_join(item).on(
|
||||||
|
website_item.item_code == item.item_code
|
||||||
|
).set(
|
||||||
|
website_item.thumbnail, item.thumbnail
|
||||||
|
).where(
|
||||||
|
website_item.website_image.notnull()
|
||||||
|
& website_item.thumbnail.isnull()
|
||||||
|
).run()
|
@ -0,0 +1,15 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
homepage = frappe.get_doc("Homepage")
|
||||||
|
|
||||||
|
for row in homepage.products:
|
||||||
|
web_item = frappe.db.get_value("Website Item", {"item_code": row.item_code}, "name")
|
||||||
|
if not web_item:
|
||||||
|
continue
|
||||||
|
|
||||||
|
row.item_code = web_item
|
||||||
|
|
||||||
|
homepage.flags.ignore_mandatory = True
|
||||||
|
homepage.save()
|
62
erpnext/patches/v13_0/populate_e_commerce_settings.py
Normal file
62
erpnext/patches/v13_0/populate_e_commerce_settings.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.utils import cint
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc("e_commerce", "doctype", "e_commerce_settings")
|
||||||
|
frappe.reload_doc("portal", "doctype", "website_filter_field")
|
||||||
|
frappe.reload_doc("portal", "doctype", "website_attribute")
|
||||||
|
|
||||||
|
products_settings_fields = [
|
||||||
|
"hide_variants", "products_per_page",
|
||||||
|
"enable_attribute_filters", "enable_field_filters"
|
||||||
|
]
|
||||||
|
|
||||||
|
shopping_cart_settings_fields = [
|
||||||
|
"enabled", "show_attachments", "show_price",
|
||||||
|
"show_stock_availability", "enable_variants", "show_contact_us_button",
|
||||||
|
"show_quantity_in_website", "show_apply_coupon_code_in_website",
|
||||||
|
"allow_items_not_in_stock", "company", "price_list", "default_customer_group",
|
||||||
|
"quotation_series", "enable_checkout", "payment_success_url",
|
||||||
|
"payment_gateway_account", "save_quotations_as_draft"
|
||||||
|
]
|
||||||
|
|
||||||
|
settings = frappe.get_doc("E Commerce Settings")
|
||||||
|
|
||||||
|
def map_into_e_commerce_settings(doctype, fields):
|
||||||
|
singles = frappe.qb.DocType("Singles")
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(singles)
|
||||||
|
.select(
|
||||||
|
singles["field"], singles.value
|
||||||
|
).where(
|
||||||
|
(singles.doctype == doctype)
|
||||||
|
& (singles["field"].isin(fields))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
data = query.run(as_dict=True)
|
||||||
|
|
||||||
|
# {'enable_attribute_filters': '1', ...}
|
||||||
|
mapper = {row.field: row.value for row in data}
|
||||||
|
|
||||||
|
for key, value in mapper.items():
|
||||||
|
value = cint(value) if (value and value.isdigit()) else value
|
||||||
|
settings.update({key: value})
|
||||||
|
|
||||||
|
settings.save()
|
||||||
|
|
||||||
|
# shift data to E Commerce Settings
|
||||||
|
map_into_e_commerce_settings("Products Settings", products_settings_fields)
|
||||||
|
map_into_e_commerce_settings("Shopping Cart Settings", shopping_cart_settings_fields)
|
||||||
|
|
||||||
|
# move filters and attributes tables to E Commerce Settings from Products Settings
|
||||||
|
for doctype in ("Website Filter Field", "Website Attribute"):
|
||||||
|
frappe.db.set_value(
|
||||||
|
doctype,
|
||||||
|
{"parent": "Products Settings"},
|
||||||
|
{
|
||||||
|
"parenttype": "E Commerce Settings",
|
||||||
|
"parent": "E Commerce Settings"
|
||||||
|
},
|
||||||
|
update_modified=False
|
||||||
|
)
|
29
erpnext/patches/v13_0/shopping_cart_to_ecommerce.py
Normal file
29
erpnext/patches/v13_0/shopping_cart_to_ecommerce.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import click
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
|
||||||
|
frappe.delete_doc("DocType", "Shopping Cart Settings", ignore_missing=True)
|
||||||
|
frappe.delete_doc("DocType", "Products Settings", ignore_missing=True)
|
||||||
|
frappe.delete_doc("DocType", "Supplier Item Group", ignore_missing=True)
|
||||||
|
|
||||||
|
if frappe.db.get_single_value("E Commerce Settings", "enabled"):
|
||||||
|
notify_users()
|
||||||
|
|
||||||
|
|
||||||
|
def notify_users():
|
||||||
|
|
||||||
|
click.secho(
|
||||||
|
"Shopping cart and Product settings are merged into E-commerce settings.\n"
|
||||||
|
"Checkout the documentation to learn more:"
|
||||||
|
"https://docs.erpnext.com/docs/v13/user/manual/en/e_commerce/set_up_e_commerce",
|
||||||
|
fg="yellow",
|
||||||
|
)
|
||||||
|
|
||||||
|
note = frappe.new_doc("Note")
|
||||||
|
note.title = "New E-Commerce Module"
|
||||||
|
note.public = 1
|
||||||
|
note.notify_on_login = 1
|
||||||
|
note.content = """<div class="ql-editor read-mode"><p>You are seeing this message because Shopping Cart is enabled on your site. </p><p><br></p><p>Shopping Cart Settings and Products settings are now merged into "E Commerce Settings". </p><p><br></p><p>You can learn about new and improved E-Commerce features in the official documentation.</p><ol><li data-list="bullet"><span class="ql-ui" contenteditable="false"></span><a href="https://docs.erpnext.com/docs/v13/user/manual/en/e_commerce/set_up_e_commerce" rel="noopener noreferrer">https://docs.erpnext.com/docs/v13/user/manual/en/e_commerce/set_up_e_commerce</a></li></ol><p><br></p></div>"""
|
||||||
|
note.insert(ignore_mandatory=True)
|
@ -3,9 +3,9 @@
|
|||||||
|
|
||||||
frappe.ui.form.on('Homepage', {
|
frappe.ui.form.on('Homepage', {
|
||||||
setup: function(frm) {
|
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 {
|
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', {
|
frappe.ui.form.on('Homepage Featured Product', {
|
||||||
|
view: function(frm, cdt, cdn) {
|
||||||
view: function(frm, cdt, cdn){
|
var child= locals[cdt][cdn];
|
||||||
var child= locals[cdt][cdn]
|
if (child.item_code && child.route) {
|
||||||
if(child.item_code && frm.doc.products_url){
|
window.open('/' + child.route, '_blank');
|
||||||
window.location.href = frm.doc.products_url + '/' + encodeURIComponent(child.item_code);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,518 +1,143 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_events_in_timeline": 0,
|
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"autoname": "",
|
|
||||||
"beta": 1,
|
"beta": 1,
|
||||||
"creation": "2016-04-22 05:27:52.109319",
|
"creation": "2016-04-22 05:27:52.109319",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "Setup",
|
"document_type": "Setup",
|
||||||
"editable_grid": 0,
|
|
||||||
"engine": "InnoDB",
|
"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": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "company",
|
"fieldname": "company",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"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
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "hero_section_based_on",
|
"fieldname": "hero_section_based_on",
|
||||||
"fieldtype": "Select",
|
"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",
|
"label": "Hero Section Based On",
|
||||||
"length": 0,
|
"options": "Default\nSlideshow\nHomepage Section"
|
||||||
"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
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_2",
|
"fieldname": "column_break_2",
|
||||||
"fieldtype": "Column Break",
|
"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,
|
|
||||||
"depends_on": "",
|
|
||||||
"fieldname": "title",
|
"fieldname": "title",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 0,
|
"label": "Title"
|
||||||
"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
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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",
|
"fieldname": "section_break_4",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"hidden": 0,
|
"label": "Hero Section"
|
||||||
"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
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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'",
|
"depends_on": "eval:doc.hero_section_based_on === 'Default'",
|
||||||
"description": "Company Tagline for website homepage",
|
"description": "Company Tagline for website homepage",
|
||||||
"fieldname": "tag_line",
|
"fieldname": "tag_line",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Tag Line",
|
"label": "Tag Line",
|
||||||
"length": 0,
|
"reqd": 1
|
||||||
"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
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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'",
|
"depends_on": "eval:doc.hero_section_based_on === 'Default'",
|
||||||
"description": "Company Description for website homepage",
|
"description": "Company Description for website homepage",
|
||||||
"fieldname": "description",
|
"fieldname": "description",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Description",
|
"label": "Description",
|
||||||
"length": 0,
|
"reqd": 1
|
||||||
"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
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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'",
|
"depends_on": "eval:doc.hero_section_based_on === 'Default'",
|
||||||
"fieldname": "hero_image",
|
"fieldname": "hero_image",
|
||||||
"fieldtype": "Attach Image",
|
"fieldtype": "Attach Image",
|
||||||
"hidden": 0,
|
"label": "Hero Image"
|
||||||
"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
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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'",
|
"depends_on": "eval:doc.hero_section_based_on === 'Slideshow'",
|
||||||
"description": "",
|
|
||||||
"fieldname": "slideshow",
|
"fieldname": "slideshow",
|
||||||
"fieldtype": "Link",
|
"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",
|
"label": "Homepage Slideshow",
|
||||||
"length": 0,
|
"options": "Website Slideshow"
|
||||||
"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
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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'",
|
"depends_on": "eval:doc.hero_section_based_on === 'Homepage Section'",
|
||||||
"fieldname": "hero_section",
|
"fieldname": "hero_section",
|
||||||
"fieldtype": "Link",
|
"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",
|
"label": "Homepage Section",
|
||||||
"length": 0,
|
"options": "Homepage Section"
|
||||||
"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
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"depends_on": "",
|
|
||||||
"fieldname": "products_section",
|
"fieldname": "products_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"hidden": 0,
|
"label": "Products"
|
||||||
"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
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"default": "/all-products",
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "/products",
|
|
||||||
"fieldname": "products_url",
|
"fieldname": "products_url",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 0,
|
"label": "URL for \"All Products\""
|
||||||
"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
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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",
|
"description": "Products to be shown on website homepage",
|
||||||
"fieldname": "products",
|
"fieldname": "products",
|
||||||
"fieldtype": "Table",
|
"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",
|
"label": "Products",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Homepage Featured Product",
|
"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"
|
"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,
|
"issingle": 1,
|
||||||
"istable": 0,
|
"links": [],
|
||||||
"max_attachments": 0,
|
"modified": "2021-02-18 13:29:29.531639",
|
||||||
"modified": "2019-03-02 23:12:59.676202",
|
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Portal",
|
"module": "Portal",
|
||||||
"name": "Homepage",
|
"name": "Homepage",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 0,
|
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 0,
|
|
||||||
"role": "System Manager",
|
"role": "System Manager",
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 0,
|
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 0,
|
|
||||||
"role": "Administrator",
|
"role": "Administrator",
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"title_field": "company",
|
"title_field": "company",
|
||||||
"track_changes": 1,
|
"track_changes": 1
|
||||||
"track_seen": 0,
|
|
||||||
"track_views": 0
|
|
||||||
}
|
}
|
@ -14,12 +14,14 @@ class Homepage(Document):
|
|||||||
delete_page_cache('home')
|
delete_page_cache('home')
|
||||||
|
|
||||||
def setup_items(self):
|
def setup_items(self):
|
||||||
for d in frappe.get_all('Item', fields=['name', 'item_name', 'description', 'image'],
|
for d in frappe.get_all('Website Item', fields=['name', 'item_name', 'description', 'image', 'route'],
|
||||||
filters={'show_in_website': 1}, limit=3):
|
filters={'published': 1}, limit=3):
|
||||||
|
|
||||||
doc = frappe.get_doc('Item', d.name)
|
doc = frappe.get_doc('Website Item', d.name)
|
||||||
if not doc.route:
|
if not doc.route:
|
||||||
# set missing route
|
# set missing route
|
||||||
doc.save()
|
doc.save()
|
||||||
self.append('products', dict(item_code=d.name,
|
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))
|
||||||
|
|
||||||
|
@ -25,10 +25,10 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_filter": 1,
|
"in_filter": 1,
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Item Code",
|
"label": "Item",
|
||||||
"oldfieldname": "item_code",
|
"oldfieldname": "item_code",
|
||||||
"oldfieldtype": "Link",
|
"oldfieldtype": "Link",
|
||||||
"options": "Item",
|
"options": "Website Item",
|
||||||
"print_width": "150px",
|
"print_width": "150px",
|
||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"search_index": 1,
|
"search_index": 1,
|
||||||
@ -63,7 +63,7 @@
|
|||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "section_break_5",
|
"fieldname": "section_break_5",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Description"
|
"label": "Details"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fetch_from": "item_code.web_long_description",
|
"fetch_from": "item_code.web_long_description",
|
||||||
@ -89,12 +89,14 @@
|
|||||||
"label": "Image"
|
"label": "Image"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"fetch_from": "item_code.thumbnail",
|
||||||
"fieldname": "thumbnail",
|
"fieldname": "thumbnail",
|
||||||
"fieldtype": "Attach Image",
|
"fieldtype": "Attach Image",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Thumbnail"
|
"label": "Thumbnail"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"fetch_from": "item_code.route",
|
||||||
"fieldname": "route",
|
"fieldname": "route",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Small Text",
|
||||||
"label": "route",
|
"label": "route",
|
||||||
@ -104,7 +106,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-08-25 15:27:49.573537",
|
"modified": "2021-02-18 13:05:50.669311",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Portal",
|
"module": "Portal",
|
||||||
"name": "Homepage Featured Product",
|
"name": "Homepage Featured Product",
|
||||||
|
@ -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
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
@ -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
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe import _
|
|
||||||
from frappe.model.document import Document
|
|
||||||
from frappe.utils import cint
|
|
||||||
|
|
||||||
|
|
||||||
class ProductsSettings(Document):
|
|
||||||
def validate(self):
|
|
||||||
if self.home_page_is_products:
|
|
||||||
frappe.db.set_value("Website Settings", None, "home_page", "products")
|
|
||||||
elif frappe.db.get_single_value("Website Settings", "home_page") == 'products':
|
|
||||||
frappe.db.set_value("Website Settings", None, "home_page", "home")
|
|
||||||
|
|
||||||
self.validate_field_filters()
|
|
||||||
self.validate_attribute_filters()
|
|
||||||
frappe.clear_document_cache("Product Settings", "Product Settings")
|
|
||||||
|
|
||||||
def validate_field_filters(self):
|
|
||||||
if not (self.enable_field_filters and self.filter_fields): return
|
|
||||||
|
|
||||||
item_meta = frappe.get_meta('Item')
|
|
||||||
valid_fields = [df.fieldname for df in item_meta.fields if df.fieldtype in ['Link', 'Table MultiSelect']]
|
|
||||||
|
|
||||||
for f in self.filter_fields:
|
|
||||||
if f.fieldname not in valid_fields:
|
|
||||||
frappe.throw(_('Filter Fields Row #{0}: Fieldname <b>{1}</b> must be of type "Link" or "Table MultiSelect"').format(f.idx, f.fieldname))
|
|
||||||
|
|
||||||
def validate_attribute_filters(self):
|
|
||||||
if not (self.enable_attribute_filters and self.filter_attributes): return
|
|
||||||
|
|
||||||
# if attribute filters are enabled, hide_variants should be disabled
|
|
||||||
self.hide_variants = 0
|
|
||||||
|
|
||||||
|
|
||||||
def home_page_is_products(doc, method):
|
|
||||||
'''Called on saving Website Settings'''
|
|
||||||
home_page_is_products = cint(frappe.db.get_single_value('Products Settings', 'home_page_is_products'))
|
|
||||||
if home_page_is_products:
|
|
||||||
doc.home_page = 'products'
|
|
@ -1,8 +0,0 @@
|
|||||||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
|
||||||
# See license.txt
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
|
|
||||||
class TestProductsSettings(unittest.TestCase):
|
|
||||||
pass
|
|
@ -1,76 +1,32 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_events_in_timeline": 0,
|
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2019-01-01 13:04:54.479079",
|
"creation": "2019-01-01 13:04:54.479079",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"attribute"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "attribute",
|
"fieldname": "attribute",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Attribute",
|
"label": "Attribute",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Item Attribute",
|
"options": "Item Attribute",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"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,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"links": [],
|
||||||
"modified": "2019-01-01 13:04:59.715572",
|
"modified": "2021-02-18 13:18:57.810536",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Portal",
|
"module": "Portal",
|
||||||
"name": "Website Attribute",
|
"name": "Website Attribute",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1,
|
"track_changes": 1
|
||||||
"track_seen": 0,
|
|
||||||
"track_views": 0
|
|
||||||
}
|
}
|
@ -1,143 +0,0 @@
|
|||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from frappe.utils import get_html_for_route
|
|
||||||
|
|
||||||
from erpnext.portal.product_configurator.utils import get_products_for_website
|
|
||||||
|
|
||||||
test_dependencies = ["Item"]
|
|
||||||
|
|
||||||
class TestProductConfigurator(unittest.TestCase):
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
cls.create_variant_item()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_variant_item(cls):
|
|
||||||
if not frappe.db.exists('Item', '_Test Variant Item - 2XL'):
|
|
||||||
frappe.get_doc({
|
|
||||||
"description": "_Test Variant Item - 2XL",
|
|
||||||
"item_code": "_Test Variant Item - 2XL",
|
|
||||||
"item_name": "_Test Variant Item - 2XL",
|
|
||||||
"doctype": "Item",
|
|
||||||
"is_stock_item": 1,
|
|
||||||
"variant_of": "_Test Variant Item",
|
|
||||||
"item_group": "_Test Item Group",
|
|
||||||
"stock_uom": "_Test UOM",
|
|
||||||
"item_defaults": [{
|
|
||||||
"company": "_Test Company",
|
|
||||||
"default_warehouse": "_Test Warehouse - _TC",
|
|
||||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
|
||||||
"buying_cost_center": "_Test Cost Center - _TC",
|
|
||||||
"selling_cost_center": "_Test Cost Center - _TC",
|
|
||||||
"income_account": "Sales - _TC"
|
|
||||||
}],
|
|
||||||
"attributes": [
|
|
||||||
{
|
|
||||||
"attribute": "Test Size",
|
|
||||||
"attribute_value": "2XL"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"show_variant_in_website": 1
|
|
||||||
}).insert()
|
|
||||||
|
|
||||||
def create_regular_web_item(self, name, item_group=None):
|
|
||||||
if not frappe.db.exists('Item', name):
|
|
||||||
doc = frappe.get_doc({
|
|
||||||
"description": name,
|
|
||||||
"item_code": name,
|
|
||||||
"item_name": name,
|
|
||||||
"doctype": "Item",
|
|
||||||
"is_stock_item": 1,
|
|
||||||
"item_group": item_group or "_Test Item Group",
|
|
||||||
"stock_uom": "_Test UOM",
|
|
||||||
"item_defaults": [{
|
|
||||||
"company": "_Test Company",
|
|
||||||
"default_warehouse": "_Test Warehouse - _TC",
|
|
||||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
|
||||||
"buying_cost_center": "_Test Cost Center - _TC",
|
|
||||||
"selling_cost_center": "_Test Cost Center - _TC",
|
|
||||||
"income_account": "Sales - _TC"
|
|
||||||
}],
|
|
||||||
"show_in_website": 1
|
|
||||||
}).insert()
|
|
||||||
else:
|
|
||||||
doc = frappe.get_doc("Item", name)
|
|
||||||
return doc
|
|
||||||
|
|
||||||
def test_product_list(self):
|
|
||||||
template_items = frappe.get_all('Item', {'show_in_website': 1})
|
|
||||||
variant_items = frappe.get_all('Item', {'show_variant_in_website': 1})
|
|
||||||
|
|
||||||
products_settings = frappe.get_doc('Products Settings')
|
|
||||||
products_settings.enable_field_filters = 1
|
|
||||||
products_settings.append('filter_fields', {'fieldname': 'item_group'})
|
|
||||||
products_settings.append('filter_fields', {'fieldname': 'stock_uom'})
|
|
||||||
products_settings.save()
|
|
||||||
|
|
||||||
html = get_html_for_route('all-products')
|
|
||||||
|
|
||||||
soup = BeautifulSoup(html, 'html.parser')
|
|
||||||
products_list = soup.find(class_='products-list')
|
|
||||||
items = products_list.find_all(class_='card')
|
|
||||||
self.assertEqual(len(items), len(template_items + variant_items))
|
|
||||||
|
|
||||||
items_with_item_group = frappe.get_all('Item', {'item_group': '_Test Item Group Desktops', 'show_in_website': 1})
|
|
||||||
variants_with_item_group = frappe.get_all('Item', {'item_group': '_Test Item Group Desktops', 'show_variant_in_website': 1})
|
|
||||||
|
|
||||||
# mock query params
|
|
||||||
frappe.form_dict = frappe._dict({
|
|
||||||
'field_filters': '{"item_group":["_Test Item Group Desktops"]}'
|
|
||||||
})
|
|
||||||
html = get_html_for_route('all-products')
|
|
||||||
soup = BeautifulSoup(html, 'html.parser')
|
|
||||||
products_list = soup.find(class_='products-list')
|
|
||||||
items = products_list.find_all(class_='card')
|
|
||||||
self.assertEqual(len(items), len(items_with_item_group + variants_with_item_group))
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_products_for_website(self):
|
|
||||||
items = get_products_for_website(attribute_filters={
|
|
||||||
'Test Size': ['2XL']
|
|
||||||
})
|
|
||||||
self.assertEqual(len(items), 1)
|
|
||||||
|
|
||||||
def test_products_in_multiple_item_groups(self):
|
|
||||||
"""Check if product is visible on multiple item group pages barring its own."""
|
|
||||||
from erpnext.shopping_cart.product_query import ProductQuery
|
|
||||||
|
|
||||||
if not frappe.db.exists("Item Group", {"name": "Tech Items"}):
|
|
||||||
item_group_doc = frappe.get_doc({
|
|
||||||
"doctype": "Item Group",
|
|
||||||
"item_group_name": "Tech Items",
|
|
||||||
"parent_item_group": "All Item Groups",
|
|
||||||
"show_in_website": 1
|
|
||||||
}).insert()
|
|
||||||
else:
|
|
||||||
item_group_doc = frappe.get_doc("Item Group", "Tech Items")
|
|
||||||
|
|
||||||
doc = self.create_regular_web_item("Portal Item", item_group="Tech Items")
|
|
||||||
if not frappe.db.exists("Website Item Group", {"parent": "Portal Item"}):
|
|
||||||
doc.append("website_item_groups", {
|
|
||||||
"item_group": "_Test Item Group Desktops"
|
|
||||||
})
|
|
||||||
doc.save()
|
|
||||||
|
|
||||||
# check if item is visible in its own Item Group's page
|
|
||||||
engine = ProductQuery()
|
|
||||||
items = engine.query({}, {"item_group": "Tech Items"}, None, start=0, item_group="Tech Items")
|
|
||||||
self.assertEqual(len(items), 1)
|
|
||||||
self.assertEqual(items[0].item_code, "Portal Item")
|
|
||||||
|
|
||||||
# check if item is visible in configured foreign Item Group's page
|
|
||||||
engine = ProductQuery()
|
|
||||||
items = engine.query({}, {"item_group": "_Test Item Group Desktops"}, None, start=0, item_group="_Test Item Group Desktops")
|
|
||||||
item_codes = [row.item_code for row in items]
|
|
||||||
|
|
||||||
self.assertIn(len(items), [2, 3])
|
|
||||||
self.assertIn("Portal Item", item_codes)
|
|
||||||
|
|
||||||
# teardown
|
|
||||||
doc.delete()
|
|
||||||
item_group_doc.delete()
|
|
@ -1,446 +0,0 @@
|
|||||||
import frappe
|
|
||||||
from frappe.utils import cint
|
|
||||||
|
|
||||||
from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager
|
|
||||||
from erpnext.setup.doctype.item_group.item_group import get_child_groups
|
|
||||||
from erpnext.shopping_cart.product_info import get_product_info_for_website
|
|
||||||
|
|
||||||
|
|
||||||
def get_field_filter_data():
|
|
||||||
product_settings = get_product_settings()
|
|
||||||
filter_fields = [row.fieldname for row in product_settings.filter_fields]
|
|
||||||
|
|
||||||
meta = frappe.get_meta('Item')
|
|
||||||
fields = [df for df in meta.fields if df.fieldname in filter_fields]
|
|
||||||
|
|
||||||
filter_data = []
|
|
||||||
for f in fields:
|
|
||||||
doctype = f.get_link_doctype()
|
|
||||||
|
|
||||||
# apply enable/disable/show_in_website filter
|
|
||||||
meta = frappe.get_meta(doctype)
|
|
||||||
filters = {}
|
|
||||||
if meta.has_field('enabled'):
|
|
||||||
filters['enabled'] = 1
|
|
||||||
if meta.has_field('disabled'):
|
|
||||||
filters['disabled'] = 0
|
|
||||||
if meta.has_field('show_in_website'):
|
|
||||||
filters['show_in_website'] = 1
|
|
||||||
|
|
||||||
values = [d.name for d in frappe.get_all(doctype, filters)]
|
|
||||||
filter_data.append([f, values])
|
|
||||||
|
|
||||||
return filter_data
|
|
||||||
|
|
||||||
|
|
||||||
def get_attribute_filter_data():
|
|
||||||
product_settings = get_product_settings()
|
|
||||||
attributes = [row.attribute for row in product_settings.filter_attributes]
|
|
||||||
attribute_docs = [
|
|
||||||
frappe.get_doc('Item Attribute', attribute) for attribute in attributes
|
|
||||||
]
|
|
||||||
|
|
||||||
# mark attribute values as checked if they are present in the request url
|
|
||||||
if frappe.form_dict:
|
|
||||||
for attr in attribute_docs:
|
|
||||||
if attr.name in frappe.form_dict:
|
|
||||||
value = frappe.form_dict[attr.name]
|
|
||||||
if value:
|
|
||||||
enabled_values = value.split(',')
|
|
||||||
else:
|
|
||||||
enabled_values = []
|
|
||||||
|
|
||||||
for v in enabled_values:
|
|
||||||
for item_attribute_row in attr.item_attribute_values:
|
|
||||||
if v == item_attribute_row.attribute_value:
|
|
||||||
item_attribute_row.checked = True
|
|
||||||
|
|
||||||
return attribute_docs
|
|
||||||
|
|
||||||
|
|
||||||
def get_products_for_website(field_filters=None, attribute_filters=None, search=None):
|
|
||||||
if attribute_filters:
|
|
||||||
item_codes = get_item_codes_by_attributes(attribute_filters)
|
|
||||||
items_by_attributes = get_items([['name', 'in', item_codes]])
|
|
||||||
|
|
||||||
if field_filters:
|
|
||||||
items_by_fields = get_items_by_fields(field_filters)
|
|
||||||
|
|
||||||
if attribute_filters and not field_filters:
|
|
||||||
return items_by_attributes
|
|
||||||
|
|
||||||
if field_filters and not attribute_filters:
|
|
||||||
return items_by_fields
|
|
||||||
|
|
||||||
if field_filters and attribute_filters:
|
|
||||||
items_intersection = []
|
|
||||||
item_codes_in_attribute = [item.name for item in items_by_attributes]
|
|
||||||
|
|
||||||
for item in items_by_fields:
|
|
||||||
if item.name in item_codes_in_attribute:
|
|
||||||
items_intersection.append(item)
|
|
||||||
|
|
||||||
return items_intersection
|
|
||||||
|
|
||||||
if search:
|
|
||||||
return get_items(search=search)
|
|
||||||
|
|
||||||
return get_items()
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
|
||||||
def get_products_html_for_website(field_filters=None, attribute_filters=None):
|
|
||||||
field_filters = frappe.parse_json(field_filters)
|
|
||||||
attribute_filters = frappe.parse_json(attribute_filters)
|
|
||||||
set_item_group_filters(field_filters)
|
|
||||||
|
|
||||||
items = get_products_for_website(field_filters, attribute_filters)
|
|
||||||
html = ''.join(get_html_for_items(items))
|
|
||||||
|
|
||||||
if not items:
|
|
||||||
html = frappe.render_template('erpnext/www/all-products/not_found.html', {})
|
|
||||||
|
|
||||||
return html
|
|
||||||
|
|
||||||
def set_item_group_filters(field_filters):
|
|
||||||
if field_filters is not None and 'item_group' in field_filters:
|
|
||||||
field_filters['item_group'] = [ig[0] for ig in get_child_groups(field_filters['item_group'])]
|
|
||||||
|
|
||||||
|
|
||||||
def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
|
|
||||||
items = []
|
|
||||||
|
|
||||||
for attribute, values in attribute_filters.items():
|
|
||||||
attribute_values = values
|
|
||||||
|
|
||||||
if not isinstance(attribute_values, list):
|
|
||||||
attribute_values = [attribute_values]
|
|
||||||
|
|
||||||
if not attribute_values: continue
|
|
||||||
|
|
||||||
wheres = []
|
|
||||||
query_values = []
|
|
||||||
for attribute_value in attribute_values:
|
|
||||||
wheres.append('( attribute = %s and attribute_value = %s )')
|
|
||||||
query_values += [attribute, attribute_value]
|
|
||||||
|
|
||||||
attribute_query = ' or '.join(wheres)
|
|
||||||
|
|
||||||
if template_item_code:
|
|
||||||
variant_of_query = 'AND t2.variant_of = %s'
|
|
||||||
query_values.append(template_item_code)
|
|
||||||
else:
|
|
||||||
variant_of_query = ''
|
|
||||||
|
|
||||||
query = '''
|
|
||||||
SELECT
|
|
||||||
t1.parent
|
|
||||||
FROM
|
|
||||||
`tabItem Variant Attribute` t1
|
|
||||||
WHERE
|
|
||||||
1 = 1
|
|
||||||
AND (
|
|
||||||
{attribute_query}
|
|
||||||
)
|
|
||||||
AND EXISTS (
|
|
||||||
SELECT
|
|
||||||
1
|
|
||||||
FROM
|
|
||||||
`tabItem` t2
|
|
||||||
WHERE
|
|
||||||
t2.name = t1.parent
|
|
||||||
{variant_of_query}
|
|
||||||
)
|
|
||||||
GROUP BY
|
|
||||||
t1.parent
|
|
||||||
ORDER BY
|
|
||||||
NULL
|
|
||||||
'''.format(attribute_query=attribute_query, variant_of_query=variant_of_query)
|
|
||||||
|
|
||||||
item_codes = set([r[0] for r in frappe.db.sql(query, query_values)])
|
|
||||||
items.append(item_codes)
|
|
||||||
|
|
||||||
res = list(set.intersection(*items))
|
|
||||||
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
|
||||||
def get_attributes_and_values(item_code):
|
|
||||||
'''Build a list of attributes and their possible values.
|
|
||||||
This will ignore the values upon selection of which there cannot exist one item.
|
|
||||||
'''
|
|
||||||
item_cache = ItemVariantsCacheManager(item_code)
|
|
||||||
item_variants_data = item_cache.get_item_variants_data()
|
|
||||||
|
|
||||||
attributes = get_item_attributes(item_code)
|
|
||||||
attribute_list = [a.attribute for a in attributes]
|
|
||||||
|
|
||||||
valid_options = {}
|
|
||||||
for item_code, attribute, attribute_value in item_variants_data:
|
|
||||||
if attribute in attribute_list:
|
|
||||||
valid_options.setdefault(attribute, set()).add(attribute_value)
|
|
||||||
|
|
||||||
item_attribute_values = frappe.db.get_all('Item Attribute Value',
|
|
||||||
['parent', 'attribute_value', 'idx'], order_by='parent asc, idx asc')
|
|
||||||
ordered_attribute_value_map = frappe._dict()
|
|
||||||
for iv in item_attribute_values:
|
|
||||||
ordered_attribute_value_map.setdefault(iv.parent, []).append(iv.attribute_value)
|
|
||||||
|
|
||||||
# build attribute values in idx order
|
|
||||||
for attr in attributes:
|
|
||||||
valid_attribute_values = valid_options.get(attr.attribute, [])
|
|
||||||
ordered_values = ordered_attribute_value_map.get(attr.attribute, [])
|
|
||||||
attr['values'] = [v for v in ordered_values if v in valid_attribute_values]
|
|
||||||
|
|
||||||
return attributes
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
|
||||||
def get_next_attribute_and_values(item_code, selected_attributes):
|
|
||||||
'''Find the count of Items that match the selected attributes.
|
|
||||||
Also, find the attribute values that are not applicable for further searching.
|
|
||||||
If less than equal to 10 items are found, return item_codes of those items.
|
|
||||||
If one item is matched exactly, return item_code of that item.
|
|
||||||
'''
|
|
||||||
selected_attributes = frappe.parse_json(selected_attributes)
|
|
||||||
|
|
||||||
item_cache = ItemVariantsCacheManager(item_code)
|
|
||||||
item_variants_data = item_cache.get_item_variants_data()
|
|
||||||
|
|
||||||
attributes = get_item_attributes(item_code)
|
|
||||||
attribute_list = [a.attribute for a in attributes]
|
|
||||||
filtered_items = get_items_with_selected_attributes(item_code, selected_attributes)
|
|
||||||
|
|
||||||
next_attribute = None
|
|
||||||
|
|
||||||
for attribute in attribute_list:
|
|
||||||
if attribute not in selected_attributes:
|
|
||||||
next_attribute = attribute
|
|
||||||
break
|
|
||||||
|
|
||||||
valid_options_for_attributes = frappe._dict({})
|
|
||||||
|
|
||||||
for a in attribute_list:
|
|
||||||
valid_options_for_attributes[a] = set()
|
|
||||||
|
|
||||||
selected_attribute = selected_attributes.get(a, None)
|
|
||||||
if selected_attribute:
|
|
||||||
# already selected attribute values are valid options
|
|
||||||
valid_options_for_attributes[a].add(selected_attribute)
|
|
||||||
|
|
||||||
for row in item_variants_data:
|
|
||||||
item_code, attribute, attribute_value = row
|
|
||||||
if item_code in filtered_items and attribute not in selected_attributes and attribute in attribute_list:
|
|
||||||
valid_options_for_attributes[attribute].add(attribute_value)
|
|
||||||
|
|
||||||
optional_attributes = item_cache.get_optional_attributes()
|
|
||||||
exact_match = []
|
|
||||||
# search for exact match if all selected attributes are required attributes
|
|
||||||
if len(selected_attributes.keys()) >= (len(attribute_list) - len(optional_attributes)):
|
|
||||||
item_attribute_value_map = item_cache.get_item_attribute_value_map()
|
|
||||||
for item_code, attr_dict in item_attribute_value_map.items():
|
|
||||||
if item_code in filtered_items and set(attr_dict.keys()) == set(selected_attributes.keys()):
|
|
||||||
exact_match.append(item_code)
|
|
||||||
|
|
||||||
filtered_items_count = len(filtered_items)
|
|
||||||
|
|
||||||
# get product info if exact match
|
|
||||||
from erpnext.shopping_cart.product_info import get_product_info_for_website
|
|
||||||
if exact_match:
|
|
||||||
data = get_product_info_for_website(exact_match[0])
|
|
||||||
product_info = data.product_info
|
|
||||||
if product_info:
|
|
||||||
product_info["allow_items_not_in_stock"] = cint(data.cart_settings.allow_items_not_in_stock)
|
|
||||||
if not data.cart_settings.show_price:
|
|
||||||
product_info = None
|
|
||||||
else:
|
|
||||||
product_info = None
|
|
||||||
|
|
||||||
return {
|
|
||||||
'next_attribute': next_attribute,
|
|
||||||
'valid_options_for_attributes': valid_options_for_attributes,
|
|
||||||
'filtered_items_count': filtered_items_count,
|
|
||||||
'filtered_items': filtered_items if filtered_items_count < 10 else [],
|
|
||||||
'exact_match': exact_match,
|
|
||||||
'product_info': product_info
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_items_with_selected_attributes(item_code, selected_attributes):
|
|
||||||
item_cache = ItemVariantsCacheManager(item_code)
|
|
||||||
attribute_value_item_map = item_cache.get_attribute_value_item_map()
|
|
||||||
|
|
||||||
items = []
|
|
||||||
for attribute, value in selected_attributes.items():
|
|
||||||
filtered_items = attribute_value_item_map.get((attribute, value), [])
|
|
||||||
items.append(set(filtered_items))
|
|
||||||
|
|
||||||
return set.intersection(*items)
|
|
||||||
|
|
||||||
|
|
||||||
def get_items_by_fields(field_filters):
|
|
||||||
meta = frappe.get_meta('Item')
|
|
||||||
filters = []
|
|
||||||
for fieldname, values in field_filters.items():
|
|
||||||
if not values: continue
|
|
||||||
|
|
||||||
_doctype = 'Item'
|
|
||||||
_fieldname = fieldname
|
|
||||||
|
|
||||||
df = meta.get_field(fieldname)
|
|
||||||
if df.fieldtype == 'Table MultiSelect':
|
|
||||||
child_doctype = df.options
|
|
||||||
child_meta = frappe.get_meta(child_doctype)
|
|
||||||
fields = child_meta.get("fields", { "fieldtype": "Link", "in_list_view": 1 })
|
|
||||||
if fields:
|
|
||||||
_doctype = child_doctype
|
|
||||||
_fieldname = fields[0].fieldname
|
|
||||||
|
|
||||||
if len(values) == 1:
|
|
||||||
filters.append([_doctype, _fieldname, '=', values[0]])
|
|
||||||
else:
|
|
||||||
filters.append([_doctype, _fieldname, 'in', values])
|
|
||||||
|
|
||||||
return get_items(filters)
|
|
||||||
|
|
||||||
|
|
||||||
def get_items(filters=None, search=None):
|
|
||||||
start = frappe.form_dict.get('start', 0)
|
|
||||||
products_settings = get_product_settings()
|
|
||||||
page_length = products_settings.products_per_page
|
|
||||||
|
|
||||||
filters = filters or []
|
|
||||||
# convert to list of filters
|
|
||||||
if isinstance(filters, dict):
|
|
||||||
filters = [['Item', fieldname, '=', value] for fieldname, value in filters.items()]
|
|
||||||
|
|
||||||
enabled_items_filter = get_conditions({ 'disabled': 0 }, 'and')
|
|
||||||
|
|
||||||
show_in_website_condition = ''
|
|
||||||
if products_settings.hide_variants:
|
|
||||||
show_in_website_condition = get_conditions({'show_in_website': 1 }, 'and')
|
|
||||||
else:
|
|
||||||
show_in_website_condition = get_conditions([
|
|
||||||
['show_in_website', '=', 1],
|
|
||||||
['show_variant_in_website', '=', 1]
|
|
||||||
], 'or')
|
|
||||||
|
|
||||||
search_condition = ''
|
|
||||||
if search:
|
|
||||||
# Default fields to search from
|
|
||||||
default_fields = {'name', 'item_name', 'description', 'item_group'}
|
|
||||||
|
|
||||||
# Get meta search fields
|
|
||||||
meta = frappe.get_meta("Item")
|
|
||||||
meta_fields = set(meta.get_search_fields())
|
|
||||||
|
|
||||||
# Join the meta fields and default fields set
|
|
||||||
search_fields = default_fields.union(meta_fields)
|
|
||||||
try:
|
|
||||||
if frappe.db.count('Item', cache=True) > 50000:
|
|
||||||
search_fields.remove('description')
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Build or filters for query
|
|
||||||
search = '%{}%'.format(search)
|
|
||||||
or_filters = [[field, 'like', search] for field in search_fields]
|
|
||||||
|
|
||||||
search_condition = get_conditions(or_filters, 'or')
|
|
||||||
|
|
||||||
filter_condition = get_conditions(filters, 'and')
|
|
||||||
|
|
||||||
where_conditions = ' and '.join(
|
|
||||||
[condition for condition in [enabled_items_filter, show_in_website_condition, \
|
|
||||||
search_condition, filter_condition] if condition]
|
|
||||||
)
|
|
||||||
|
|
||||||
left_joins = []
|
|
||||||
for f in filters:
|
|
||||||
if len(f) == 4 and f[0] != 'Item':
|
|
||||||
left_joins.append(f[0])
|
|
||||||
|
|
||||||
left_join = ' '.join(['LEFT JOIN `tab{0}` on (`tab{0}`.parent = `tabItem`.name)'.format(l) for l in left_joins])
|
|
||||||
|
|
||||||
results = frappe.db.sql('''
|
|
||||||
SELECT
|
|
||||||
`tabItem`.`name`, `tabItem`.`item_name`, `tabItem`.`item_code`,
|
|
||||||
`tabItem`.`website_image`, `tabItem`.`image`,
|
|
||||||
`tabItem`.`web_long_description`, `tabItem`.`description`,
|
|
||||||
`tabItem`.`route`, `tabItem`.`item_group`
|
|
||||||
FROM
|
|
||||||
`tabItem`
|
|
||||||
{left_join}
|
|
||||||
WHERE
|
|
||||||
{where_conditions}
|
|
||||||
GROUP BY
|
|
||||||
`tabItem`.`name`
|
|
||||||
ORDER BY
|
|
||||||
`tabItem`.`weightage` DESC
|
|
||||||
LIMIT
|
|
||||||
{page_length}
|
|
||||||
OFFSET
|
|
||||||
{start}
|
|
||||||
'''.format(
|
|
||||||
where_conditions=where_conditions,
|
|
||||||
start=start,
|
|
||||||
page_length=page_length,
|
|
||||||
left_join=left_join
|
|
||||||
)
|
|
||||||
, as_dict=1)
|
|
||||||
|
|
||||||
for r in results:
|
|
||||||
r.description = r.web_long_description or r.description
|
|
||||||
r.image = r.website_image or r.image
|
|
||||||
product_info = get_product_info_for_website(r.item_code, skip_quotation_creation=True).get('product_info')
|
|
||||||
if product_info:
|
|
||||||
r.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def get_conditions(filter_list, and_or='and'):
|
|
||||||
from frappe.model.db_query import DatabaseQuery
|
|
||||||
|
|
||||||
if not filter_list:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
conditions = []
|
|
||||||
DatabaseQuery('Item').build_filter_conditions(filter_list, conditions, ignore_permissions=True)
|
|
||||||
join_by = ' {0} '.format(and_or)
|
|
||||||
|
|
||||||
return '(' + join_by.join(conditions) + ')'
|
|
||||||
|
|
||||||
# utilities
|
|
||||||
|
|
||||||
def get_item_attributes(item_code):
|
|
||||||
attributes = frappe.db.get_all('Item Variant Attribute',
|
|
||||||
fields=['attribute'],
|
|
||||||
filters={
|
|
||||||
'parenttype': 'Item',
|
|
||||||
'parent': item_code
|
|
||||||
},
|
|
||||||
order_by='idx asc'
|
|
||||||
)
|
|
||||||
|
|
||||||
optional_attributes = ItemVariantsCacheManager(item_code).get_optional_attributes()
|
|
||||||
|
|
||||||
for a in attributes:
|
|
||||||
if a.attribute in optional_attributes:
|
|
||||||
a.optional = True
|
|
||||||
|
|
||||||
return attributes
|
|
||||||
|
|
||||||
def get_html_for_items(items):
|
|
||||||
html = []
|
|
||||||
for item in items:
|
|
||||||
html.append(frappe.render_template('erpnext/www/all-products/item_row.html', {
|
|
||||||
'item': item
|
|
||||||
}))
|
|
||||||
return html
|
|
||||||
|
|
||||||
def get_product_settings():
|
|
||||||
doc = frappe.get_cached_doc('Products Settings')
|
|
||||||
doc.products_per_page = doc.products_per_page or 20
|
|
||||||
return doc
|
|
@ -1,10 +1,10 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.utils.nestedset import get_root_of
|
from frappe.utils.nestedset import get_root_of
|
||||||
|
|
||||||
from erpnext.shopping_cart.cart import get_debtors_account
|
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||||
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
|
|
||||||
get_shopping_cart_settings,
|
get_shopping_cart_settings,
|
||||||
)
|
)
|
||||||
|
from erpnext.e_commerce.shopping_cart.cart import get_debtors_account
|
||||||
|
|
||||||
|
|
||||||
def set_default_role(doc, method):
|
def set_default_role(doc, method):
|
||||||
|
@ -7,7 +7,8 @@
|
|||||||
],
|
],
|
||||||
"js/erpnext-web.min.js": [
|
"js/erpnext-web.min.js": [
|
||||||
"public/js/website_utils.js",
|
"public/js/website_utils.js",
|
||||||
"public/js/shopping_cart.js"
|
"public/js/shopping_cart.js",
|
||||||
|
"public/js/wishlist.js"
|
||||||
],
|
],
|
||||||
"css/erpnext-web.css": [
|
"css/erpnext-web.css": [
|
||||||
"public/scss/website.scss",
|
"public/scss/website.scss",
|
||||||
@ -65,5 +66,11 @@
|
|||||||
"js/hierarchy-chart.min.js": [
|
"js/hierarchy-chart.min.js": [
|
||||||
"public/js/hierarchy_chart/hierarchy_chart_desktop.js",
|
"public/js/hierarchy_chart/hierarchy_chart_desktop.js",
|
||||||
"public/js/hierarchy_chart/hierarchy_chart_mobile.js"
|
"public/js/hierarchy_chart/hierarchy_chart_mobile.js"
|
||||||
|
],
|
||||||
|
"js/e-commerce.min.js": [
|
||||||
|
"e_commerce/product_ui/views.js",
|
||||||
|
"e_commerce/product_ui/grid.js",
|
||||||
|
"e_commerce/product_ui/list.js",
|
||||||
|
"e_commerce/product_ui/search.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
// js inside blog page
|
// js inside blog page
|
||||||
|
|
||||||
// shopping cart
|
// shopping cart
|
||||||
frappe.provide("erpnext.shopping_cart");
|
frappe.provide("erpnext.e_commerce.shopping_cart");
|
||||||
var shopping_cart = erpnext.shopping_cart;
|
var shopping_cart = erpnext.e_commerce.shopping_cart;
|
||||||
|
|
||||||
$.extend(shopping_cart, {
|
$.extend(shopping_cart, {
|
||||||
show_error: function(title, text) {
|
show_error: function(title, text) {
|
||||||
@ -18,8 +18,8 @@ $.extend(shopping_cart, {
|
|||||||
shopping_cart.bind_place_order();
|
shopping_cart.bind_place_order();
|
||||||
shopping_cart.bind_request_quotation();
|
shopping_cart.bind_request_quotation();
|
||||||
shopping_cart.bind_change_qty();
|
shopping_cart.bind_change_qty();
|
||||||
|
shopping_cart.bind_remove_cart_item();
|
||||||
shopping_cart.bind_change_notes();
|
shopping_cart.bind_change_notes();
|
||||||
shopping_cart.bind_dropdown_cart_buttons();
|
|
||||||
shopping_cart.bind_coupon_code();
|
shopping_cart.bind_coupon_code();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ $.extend(shopping_cart, {
|
|||||||
const address_name = $card.closest('[data-address-name]').attr('data-address-name');
|
const address_name = $card.closest('[data-address-name]').attr('data-address-name');
|
||||||
frappe.call({
|
frappe.call({
|
||||||
type: "POST",
|
type: "POST",
|
||||||
method: "erpnext.shopping_cart.cart.update_cart_address",
|
method: "erpnext.e_commerce.shopping_cart.cart.update_cart_address",
|
||||||
freeze: true,
|
freeze: true,
|
||||||
args: {
|
args: {
|
||||||
address_type,
|
address_type,
|
||||||
@ -57,7 +57,7 @@ $.extend(shopping_cart, {
|
|||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
d.hide();
|
d.hide();
|
||||||
if (!r.exc) {
|
if (!r.exc) {
|
||||||
$(".cart-tax-items").html(r.message.taxes);
|
$(".cart-tax-items").html(r.message.total);
|
||||||
shopping_cart.parent.find(
|
shopping_cart.parent.find(
|
||||||
`.address-container[data-address-type="${address_type}"]`
|
`.address-container[data-address-type="${address_type}"]`
|
||||||
).html(r.message.address);
|
).html(r.message.address);
|
||||||
@ -129,8 +129,14 @@ $.extend(shopping_cart, {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
input.val(newVal);
|
input.val(newVal);
|
||||||
|
|
||||||
|
let notes = input.closest("td").siblings().find(".notes").text().trim();
|
||||||
var item_code = input.attr("data-item-code");
|
var item_code = input.attr("data-item-code");
|
||||||
shopping_cart.shopping_cart_update({item_code, qty: newVal});
|
shopping_cart.shopping_cart_update({
|
||||||
|
item_code,
|
||||||
|
qty: newVal,
|
||||||
|
additional_notes: notes
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -148,6 +154,18 @@ $.extend(shopping_cart, {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
bind_remove_cart_item: function() {
|
||||||
|
$(".cart-items").on("click", ".remove-cart-item", (e) => {
|
||||||
|
const $remove_cart_item_btn = $(e.currentTarget);
|
||||||
|
var item_code = $remove_cart_item_btn.data("item-code");
|
||||||
|
|
||||||
|
shopping_cart.shopping_cart_update({
|
||||||
|
item_code: item_code,
|
||||||
|
qty: 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
render_tax_row: function($cart_taxes, doc, shipping_rules) {
|
render_tax_row: function($cart_taxes, doc, shipping_rules) {
|
||||||
var shipping_selector;
|
var shipping_selector;
|
||||||
if(shipping_rules) {
|
if(shipping_rules) {
|
||||||
@ -185,7 +203,7 @@ $.extend(shopping_cart, {
|
|||||||
return frappe.call({
|
return frappe.call({
|
||||||
btn: btn,
|
btn: btn,
|
||||||
type: "POST",
|
type: "POST",
|
||||||
method: "erpnext.shopping_cart.cart.apply_shipping_rule",
|
method: "erpnext.e_commerce.shopping_cart.cart.apply_shipping_rule",
|
||||||
args: { shipping_rule: rule },
|
args: { shipping_rule: rule },
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if(!r.exc) {
|
if(!r.exc) {
|
||||||
@ -196,12 +214,15 @@ $.extend(shopping_cart, {
|
|||||||
},
|
},
|
||||||
|
|
||||||
place_order: function(btn) {
|
place_order: function(btn) {
|
||||||
|
shopping_cart.freeze();
|
||||||
|
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
type: "POST",
|
type: "POST",
|
||||||
method: "erpnext.shopping_cart.cart.place_order",
|
method: "erpnext.e_commerce.shopping_cart.cart.place_order",
|
||||||
btn: btn,
|
btn: btn,
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if(r.exc) {
|
if(r.exc) {
|
||||||
|
shopping_cart.unfreeze();
|
||||||
var msg = "";
|
var msg = "";
|
||||||
if(r._server_messages) {
|
if(r._server_messages) {
|
||||||
msg = JSON.parse(r._server_messages || []).join("<br>");
|
msg = JSON.parse(r._server_messages || []).join("<br>");
|
||||||
@ -212,7 +233,6 @@ $.extend(shopping_cart, {
|
|||||||
.html(msg || frappe._("Something went wrong!"))
|
.html(msg || frappe._("Something went wrong!"))
|
||||||
.toggle(true);
|
.toggle(true);
|
||||||
} else {
|
} else {
|
||||||
$('.cart-container table').hide();
|
|
||||||
$(btn).hide();
|
$(btn).hide();
|
||||||
window.location.href = '/orders/' + encodeURIComponent(r.message);
|
window.location.href = '/orders/' + encodeURIComponent(r.message);
|
||||||
}
|
}
|
||||||
@ -221,12 +241,15 @@ $.extend(shopping_cart, {
|
|||||||
},
|
},
|
||||||
|
|
||||||
request_quotation: function(btn) {
|
request_quotation: function(btn) {
|
||||||
|
shopping_cart.freeze();
|
||||||
|
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
type: "POST",
|
type: "POST",
|
||||||
method: "erpnext.shopping_cart.cart.request_for_quotation",
|
method: "erpnext.e_commerce.shopping_cart.cart.request_for_quotation",
|
||||||
btn: btn,
|
btn: btn,
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if(r.exc) {
|
if(r.exc) {
|
||||||
|
shopping_cart.unfreeze();
|
||||||
var msg = "";
|
var msg = "";
|
||||||
if(r._server_messages) {
|
if(r._server_messages) {
|
||||||
msg = JSON.parse(r._server_messages || []).join("<br>");
|
msg = JSON.parse(r._server_messages || []).join("<br>");
|
||||||
@ -237,7 +260,6 @@ $.extend(shopping_cart, {
|
|||||||
.html(msg || frappe._("Something went wrong!"))
|
.html(msg || frappe._("Something went wrong!"))
|
||||||
.toggle(true);
|
.toggle(true);
|
||||||
} else {
|
} else {
|
||||||
$('.cart-container table').hide();
|
|
||||||
$(btn).hide();
|
$(btn).hide();
|
||||||
window.location.href = '/quotations/' + encodeURIComponent(r.message);
|
window.location.href = '/quotations/' + encodeURIComponent(r.message);
|
||||||
}
|
}
|
||||||
@ -254,7 +276,7 @@ $.extend(shopping_cart, {
|
|||||||
apply_coupon_code: function(btn) {
|
apply_coupon_code: function(btn) {
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
type: "POST",
|
type: "POST",
|
||||||
method: "erpnext.shopping_cart.cart.apply_coupon_code",
|
method: "erpnext.e_commerce.shopping_cart.cart.apply_coupon_code",
|
||||||
btn: btn,
|
btn: btn,
|
||||||
args : {
|
args : {
|
||||||
applied_code : $('.txtcoupon').val(),
|
applied_code : $('.txtcoupon').val(),
|
||||||
@ -270,7 +292,9 @@ $.extend(shopping_cart, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
frappe.ready(function() {
|
frappe.ready(function() {
|
||||||
$(".cart-icon").hide();
|
if (window.location.pathname === "/cart") {
|
||||||
|
$(".cart-icon").hide();
|
||||||
|
}
|
||||||
shopping_cart.parent = $(".cart-container");
|
shopping_cart.parent = $(".cart-container");
|
||||||
shopping_cart.bind_events();
|
shopping_cart.bind_events();
|
||||||
});
|
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user