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",
|
||||
"income_account": "Sales - _TC"
|
||||
}],
|
||||
"show_in_website": 1,
|
||||
"route":"-test-tesla-car",
|
||||
"website_warehouse": "Stores - _TC"
|
||||
})
|
||||
item.insert()
|
||||
# create test item price
|
||||
|
@ -291,7 +291,7 @@ class PaymentRequest(Document):
|
||||
if not status:
|
||||
return
|
||||
|
||||
shopping_cart_settings = frappe.get_doc("Shopping Cart Settings")
|
||||
shopping_cart_settings = frappe.get_doc("E Commerce Settings")
|
||||
|
||||
if status in ["Authorized", "Completed"]:
|
||||
redirect_to = None
|
||||
@ -435,13 +435,13 @@ def get_existing_payment_request_amount(ref_dt, ref_dn):
|
||||
""", (ref_dt, ref_dn))
|
||||
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
|
||||
|
||||
def get_gateway_details(args):
|
||||
def get_gateway_details(args): # nosemgrep
|
||||
"""return gateway and payment account of default payment gateway"""
|
||||
if args.get("payment_gateway_account"):
|
||||
return get_payment_gateway_account(args.get("payment_gateway_account"))
|
||||
|
||||
if args.order_type == "Shopping Cart":
|
||||
payment_gateway_account = frappe.get_doc("Shopping Cart Settings").payment_gateway_account
|
||||
payment_gateway_account = frappe.get_doc("E Commerce Settings").payment_gateway_account
|
||||
return get_payment_gateway_account(payment_gateway_account)
|
||||
|
||||
gateway_account = get_payment_gateway_account({"is_default": 1})
|
||||
|
@ -98,7 +98,7 @@ class TaxRule(Document):
|
||||
def validate_use_for_shopping_cart(self):
|
||||
'''If shopping cart is enabled and no tax rule exists for shopping cart, enable this one'''
|
||||
if (not self.use_for_shopping_cart
|
||||
and cint(frappe.db.get_single_value('Shopping Cart Settings', 'enabled'))
|
||||
and cint(frappe.db.get_single_value('E Commerce Settings', 'enabled'))
|
||||
and not frappe.db.get_value('Tax Rule', {'use_for_shopping_cart': 1, 'name': ['!=', self.name]})):
|
||||
|
||||
self.use_for_shopping_cart = 1
|
||||
|
@ -131,28 +131,6 @@ class Supplier(TransactionBase):
|
||||
if frappe.defaults.get_global_default('supp_master_name') == 'Supplier Name':
|
||||
frappe.db.set(self, "supplier_name", newdn)
|
||||
|
||||
def create_onboarding_docs(self, args):
|
||||
company = frappe.defaults.get_defaults().get('company') or \
|
||||
frappe.db.get_single_value('Global Defaults', 'default_company')
|
||||
|
||||
for i in range(1, args.get('max_count')):
|
||||
supplier = args.get('supplier_name_' + str(i))
|
||||
if supplier:
|
||||
try:
|
||||
doc = frappe.get_doc({
|
||||
'doctype': self.doctype,
|
||||
'supplier_name': supplier,
|
||||
'supplier_group': _('Local'),
|
||||
'company': company
|
||||
}).insert()
|
||||
|
||||
if args.get('supplier_email_' + str(i)):
|
||||
from erpnext.selling.doctype.customer.customer import create_contact
|
||||
create_contact(supplier, 'Supplier',
|
||||
doc.name, args.get('supplier_email_' + str(i)))
|
||||
except frappe.NameError:
|
||||
pass
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
@ -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)
|
||||
|
||||
from erpnext.portal.product_configurator.utils import get_item_codes_by_attributes
|
||||
from erpnext.e_commerce.variant_selector.utils import get_item_codes_by_attributes
|
||||
possible_variants = [i for i in get_item_codes_by_attributes(args, template) if i != variant_item_code]
|
||||
|
||||
for variant in possible_variants:
|
||||
@ -262,9 +262,8 @@ def generate_keyed_value_combinations(args):
|
||||
def copy_attributes_to_variant(item, variant):
|
||||
# copy non no-copy fields
|
||||
|
||||
exclude_fields = ["naming_series", "item_code", "item_name", "show_in_website",
|
||||
"show_variant_in_website", "opening_stock", "variant_of", "valuation_rate",
|
||||
"has_variants", "attributes"]
|
||||
exclude_fields = ["naming_series", "item_code", "item_name", "published_in_website",
|
||||
"opening_stock", "variant_of", "valuation_rate"]
|
||||
|
||||
if item.variant_based_on=='Manufacturer':
|
||||
# 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
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Shopping Cart Settings", {
|
||||
frappe.ui.form.on("E Commerce Settings", {
|
||||
onload: function(frm) {
|
||||
if(frm.doc.__onload && frm.doc.__onload.quotation_series) {
|
||||
frm.fields_dict.quotation_series.df.options = frm.doc.__onload.quotation_series;
|
||||
@ -23,6 +23,21 @@ frappe.ui.form.on("Shopping Cart Settings", {
|
||||
</div>`
|
||||
);
|
||||
}
|
||||
|
||||
frappe.model.with_doctype("Item", () => {
|
||||
const web_item_meta = frappe.get_meta('Website Item');
|
||||
|
||||
const valid_fields = web_item_meta.fields.filter(
|
||||
df => ["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden
|
||||
).map(df => ({ label: df.label, value: df.fieldname }));
|
||||
|
||||
frm.fields_dict.filter_fields.grid.update_docfield_property(
|
||||
'fieldname', 'fieldtype', 'Select'
|
||||
);
|
||||
frm.fields_dict.filter_fields.grid.update_docfield_property(
|
||||
'fieldname', 'options', valid_fields
|
||||
);
|
||||
});
|
||||
},
|
||||
enabled: function(frm) {
|
||||
if (frm.doc.enabled === 1) {
|
@ -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
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
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 ShoppingCartSettings(Document):
|
||||
class ECommerceSettings(Document):
|
||||
def onload(self):
|
||||
self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series")
|
||||
self.is_redisearch_loaded = is_search_module_loaded()
|
||||
|
||||
def validate(self):
|
||||
self.validate_field_filters()
|
||||
self.validate_attribute_filters()
|
||||
self.validate_checkout()
|
||||
self.validate_search_index_fields()
|
||||
|
||||
if self.enabled:
|
||||
self.validate_price_list_exchange_rate()
|
||||
|
||||
frappe.clear_document_cache("E Commerce Settings", "E Commerce Settings")
|
||||
|
||||
def validate_field_filters(self):
|
||||
if not (self.enable_field_filters and self.filter_fields):
|
||||
return
|
||||
|
||||
item_meta = frappe.get_meta("Item")
|
||||
valid_fields = [df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]]
|
||||
|
||||
for f in self.filter_fields:
|
||||
if f.fieldname not in valid_fields:
|
||||
frappe.throw(_("Filter Fields Row #{0}: Fieldname <b>{1}</b> must be of type 'Link' or 'Table MultiSelect'").format(f.idx, f.fieldname))
|
||||
|
||||
def validate_attribute_filters(self):
|
||||
if not (self.enable_attribute_filters and self.filter_attributes):
|
||||
return
|
||||
|
||||
# if attribute filters are enabled, hide_variants should be disabled
|
||||
self.hide_variants = 0
|
||||
|
||||
def validate_checkout(self):
|
||||
if self.enable_checkout and not self.payment_gateway_account:
|
||||
self.enable_checkout = 0
|
||||
|
||||
def validate_search_index_fields(self):
|
||||
if not self.search_index_fields:
|
||||
return
|
||||
|
||||
fields = self.search_index_fields.replace(' ', '')
|
||||
fields = unique(fields.strip(',').split(',')) # Remove extra ',' and remove duplicates
|
||||
|
||||
# All fields should be indexable
|
||||
allowed_indexable_fields = get_indexable_web_fields()
|
||||
|
||||
if not (set(fields).issubset(allowed_indexable_fields)):
|
||||
invalid_fields = list(set(fields).difference(allowed_indexable_fields))
|
||||
num_invalid_fields = len(invalid_fields)
|
||||
invalid_fields = comma_and(invalid_fields)
|
||||
|
||||
if num_invalid_fields > 1:
|
||||
frappe.throw(_("{0} are not valid options for Search Index Field.").format(frappe.bold(invalid_fields)))
|
||||
else:
|
||||
frappe.throw(_("{0} is not a valid option for Search Index Field.").format(frappe.bold(invalid_fields)))
|
||||
|
||||
self.search_index_fields = ','.join(fields)
|
||||
|
||||
def validate_price_list_exchange_rate(self):
|
||||
"Check if exchange rate exists for Price List currency (to Company's currency)."
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
@ -60,12 +116,23 @@ class ShoppingCartSettings(Document):
|
||||
def get_shipping_rules(self, shipping_territory):
|
||||
return self.get_name_from_territory(shipping_territory, "shipping_rules", "shipping_rule")
|
||||
|
||||
def on_change(self):
|
||||
old_doc = self.get_doc_before_save()
|
||||
|
||||
if old_doc:
|
||||
old_fields = old_doc.search_index_fields
|
||||
new_fields = self.search_index_fields
|
||||
|
||||
# if search index fields get changed
|
||||
if not (new_fields == old_fields):
|
||||
create_website_items_index()
|
||||
|
||||
def validate_cart_settings(doc=None, method=None):
|
||||
frappe.get_doc("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():
|
||||
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
|
||||
|
@ -1,24 +1,21 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
|
||||
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||
ShoppingCartSetupError,
|
||||
)
|
||||
|
||||
|
||||
class TestShoppingCartSettings(unittest.TestCase):
|
||||
class TestECommerceSettings(unittest.TestCase):
|
||||
def setUp(self):
|
||||
frappe.db.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """)
|
||||
|
||||
def get_cart_settings(self):
|
||||
return frappe.get_doc({"doctype": "Shopping Cart Settings",
|
||||
return frappe.get_doc({"doctype": "E Commerce Settings",
|
||||
"company": "_Test Company"})
|
||||
|
||||
# NOTE: Exchangrate API has all enabled currencies that ERPNext supports.
|
||||
@ -34,15 +31,17 @@ class TestShoppingCartSettings(unittest.TestCase):
|
||||
|
||||
# cart_settings = self.get_cart_settings()
|
||||
# cart_settings.price_list = "_Test Price List Rest of the World"
|
||||
# self.assertRaises(ShoppingCartSetupError, cart_settings.validate_price_list_exchange_rate)
|
||||
# self.assertRaises(ShoppingCartSetupError, cart_settings.validate_exchange_rates_exist)
|
||||
|
||||
# from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \
|
||||
# currency_exchange_records
|
||||
# from erpnext.setup.doctype.currency_exchange.test_currency_exchange import (
|
||||
# test_records as currency_exchange_records,
|
||||
# )
|
||||
# frappe.get_doc(currency_exchange_records[0]).insert()
|
||||
# cart_settings.validate_price_list_exchange_rate()
|
||||
# cart_settings.validate_exchange_rates_exist()
|
||||
|
||||
def test_tax_rule_validation(self):
|
||||
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
|
||||
frappe.db.commit() # nosemgrep
|
||||
|
||||
cart_settings = self.get_cart_settings()
|
||||
cart_settings.enabled = 1
|
||||
@ -51,4 +50,13 @@ class TestShoppingCartSettings(unittest.TestCase):
|
||||
|
||||
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1")
|
||||
|
||||
def setup_e_commerce_settings(values_dict):
|
||||
"Accepts a dict of values that updates E Commerce Settings."
|
||||
if not values_dict:
|
||||
return
|
||||
|
||||
doc = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
|
||||
doc.update(values_dict)
|
||||
doc.save()
|
||||
|
||||
test_dependencies = ["Tax Rule"]
|
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.query import Prefix
|
||||
|
||||
# TODO: Make obsolete
|
||||
INDEX_NAME = "products"
|
||||
|
||||
class ProductSearch(FullTextSearch):
|
||||
@ -111,7 +112,7 @@ class ProductSearch(FullTextSearch):
|
||||
)
|
||||
|
||||
def get_all_published_items():
|
||||
return frappe.get_all("Item", filters={"variant_of": "", "show_in_website": 1},pluck="name")
|
||||
return frappe.get_all("Website Item", filters={"variant_of": "", "published": 1}, pluck="item_code")
|
||||
|
||||
def update_index_for_path(path):
|
||||
search = ProductSearch(INDEX_NAME)
|
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
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
from frappe import _, throw
|
||||
@ -11,20 +10,20 @@ from frappe.utils import cint, cstr, flt, get_fullname
|
||||
from frappe.utils.nestedset import get_root_of
|
||||
|
||||
from erpnext.accounts.utils import get_account_name
|
||||
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
|
||||
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||
get_shopping_cart_settings,
|
||||
)
|
||||
from erpnext.utilities.product import get_qty_in_stock
|
||||
from erpnext.utilities.product import get_web_item_qty_in_stock
|
||||
|
||||
|
||||
class WebsitePriceListMissingError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
def set_cart_count(quotation=None):
|
||||
if cint(frappe.db.get_singles_value("Shopping Cart Settings", "enabled")):
|
||||
if cint(frappe.db.get_singles_value("E Commerce Settings", "enabled")):
|
||||
if not quotation:
|
||||
quotation = _get_cart_quotation()
|
||||
cart_count = cstr(len(quotation.get("items")))
|
||||
cart_count = cstr(cint(quotation.get("total_qty")))
|
||||
|
||||
if hasattr(frappe.local, "cookie_manager"):
|
||||
frappe.local.cookie_manager.set_cookie("cart_count", cart_count)
|
||||
@ -48,7 +47,7 @@ def get_cart_quotation(doc=None):
|
||||
"shipping_addresses": get_shipping_addresses(party),
|
||||
"billing_addresses": get_billing_addresses(party),
|
||||
"shipping_rules": get_applicable_shipping_rules(party),
|
||||
"cart_settings": frappe.get_cached_doc("Shopping Cart Settings")
|
||||
"cart_settings": frappe.get_cached_doc("E Commerce Settings")
|
||||
}
|
||||
|
||||
@frappe.whitelist()
|
||||
@ -72,7 +71,7 @@ def get_billing_addresses(party=None):
|
||||
@frappe.whitelist()
|
||||
def place_order():
|
||||
quotation = _get_cart_quotation()
|
||||
cart_settings = frappe.db.get_value("Shopping Cart Settings", None,
|
||||
cart_settings = frappe.db.get_value("E Commerce Settings", None,
|
||||
["company", "allow_items_not_in_stock"], as_dict=1)
|
||||
quotation.company = cart_settings.company
|
||||
|
||||
@ -92,13 +91,19 @@ def place_order():
|
||||
|
||||
if not cint(cart_settings.allow_items_not_in_stock):
|
||||
for item in sales_order.get("items"):
|
||||
item.reserved_warehouse, is_stock_item = frappe.db.get_value("Item",
|
||||
item.item_code, ["website_warehouse", "is_stock_item"])
|
||||
item.warehouse = frappe.db.get_value(
|
||||
"Website Item",
|
||||
{
|
||||
"item_code": item.item_code
|
||||
},
|
||||
"website_warehouse"
|
||||
)
|
||||
is_stock_item = frappe.db.get_value("Item", item.item_code, "is_stock_item")
|
||||
|
||||
if is_stock_item:
|
||||
item_stock = get_qty_in_stock(item.item_code, "website_warehouse")
|
||||
item_stock = get_web_item_qty_in_stock(item.item_code, "website_warehouse")
|
||||
if not cint(item_stock.in_stock):
|
||||
throw(_("{1} Not in Stock").format(item.item_code))
|
||||
throw(_("{0} Not in Stock").format(item.item_code))
|
||||
if item.qty > item_stock.stock_qty[0][0]:
|
||||
throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code))
|
||||
|
||||
@ -156,19 +161,19 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False):
|
||||
|
||||
set_cart_count(quotation)
|
||||
|
||||
context = get_cart_quotation(quotation)
|
||||
|
||||
if cint(with_items):
|
||||
context = get_cart_quotation(quotation)
|
||||
return {
|
||||
"items": frappe.render_template("templates/includes/cart/cart_items.html",
|
||||
context),
|
||||
"taxes": frappe.render_template("templates/includes/order/order_taxes.html",
|
||||
"total": frappe.render_template("templates/includes/cart/cart_items_total.html",
|
||||
context),
|
||||
"taxes_and_totals": frappe.render_template("templates/includes/cart/cart_payment_summary.html",
|
||||
context)
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'name': quotation.name,
|
||||
'shopping_cart_menu': get_shopping_cart_menu(context)
|
||||
'name': quotation.name
|
||||
}
|
||||
|
||||
@frappe.whitelist()
|
||||
@ -265,13 +270,36 @@ def guess_territory():
|
||||
territory = frappe.db.get_value("Territory", geoip_country)
|
||||
|
||||
return territory or \
|
||||
frappe.db.get_value("Shopping Cart Settings", None, "territory") or \
|
||||
frappe.db.get_value("E Commerce Settings", None, "territory") or \
|
||||
get_root_of("Territory")
|
||||
|
||||
def decorate_quotation_doc(doc):
|
||||
for d in doc.get("items", []):
|
||||
d.update(frappe.db.get_value("Item", d.item_code,
|
||||
["thumbnail", "website_image", "description", "route"], as_dict=True))
|
||||
item_code = d.item_code
|
||||
fields = ["web_item_name", "thumbnail", "website_image", "description", "route"]
|
||||
|
||||
# Variant Item
|
||||
if not frappe.db.exists("Website Item", {"item_code": item_code}):
|
||||
variant_data = frappe.db.get_values(
|
||||
"Item",
|
||||
filters={"item_code": item_code},
|
||||
fieldname=["variant_of", "item_name", "image"],
|
||||
as_dict=True
|
||||
)[0]
|
||||
item_code = variant_data.variant_of
|
||||
fields = fields[1:]
|
||||
d.web_item_name = variant_data.item_name
|
||||
|
||||
if variant_data.image: # get image from variant or template web item
|
||||
d.thumbnail = variant_data.image
|
||||
fields = fields[2:]
|
||||
|
||||
d.update(frappe.db.get_value(
|
||||
"Website Item",
|
||||
{"item_code": item_code},
|
||||
fields,
|
||||
as_dict=True)
|
||||
)
|
||||
|
||||
return doc
|
||||
|
||||
@ -288,7 +316,7 @@ def _get_cart_quotation(party=None):
|
||||
if quotation:
|
||||
qdoc = frappe.get_doc("Quotation", quotation[0].name)
|
||||
else:
|
||||
company = frappe.db.get_value("Shopping Cart Settings", None, ["company"])
|
||||
company = frappe.db.get_value("E Commerce Settings", None, ["company"])
|
||||
qdoc = frappe.get_doc({
|
||||
"doctype": "Quotation",
|
||||
"naming_series": get_shopping_cart_settings().quotation_series or "QTN-CART-",
|
||||
@ -343,7 +371,7 @@ def apply_cart_settings(party=None, quotation=None):
|
||||
if not quotation:
|
||||
quotation = _get_cart_quotation(party)
|
||||
|
||||
cart_settings = frappe.get_doc("Shopping Cart Settings")
|
||||
cart_settings = frappe.get_doc("E Commerce Settings")
|
||||
|
||||
set_price_list_and_rate(quotation, cart_settings)
|
||||
|
||||
@ -420,7 +448,7 @@ def get_party(user=None):
|
||||
party_doctype = contact.links[0].link_doctype
|
||||
party = contact.links[0].link_name
|
||||
|
||||
cart_settings = frappe.get_doc("Shopping Cart Settings")
|
||||
cart_settings = frappe.get_doc("E Commerce Settings")
|
||||
|
||||
debtors_account = ''
|
||||
|
||||
@ -557,10 +585,20 @@ def get_shipping_rules(quotation=None, cart_settings=None):
|
||||
if quotation.shipping_address_name:
|
||||
country = frappe.db.get_value("Address", quotation.shipping_address_name, "country")
|
||||
if country:
|
||||
shipping_rules = frappe.db.sql_list("""select distinct sr.name
|
||||
from `tabShipping Rule Country` src, `tabShipping Rule` sr
|
||||
where src.country = %s and
|
||||
sr.disabled != 1 and sr.name = src.parent""", country)
|
||||
sr_country = frappe.qb.DocType("Shipping Rule Country")
|
||||
sr = frappe.qb.DocType("Shipping Rule")
|
||||
query = (
|
||||
frappe.qb.from_(sr_country)
|
||||
.join(sr).on(sr.name == sr_country.parent)
|
||||
.select(sr.name)
|
||||
.distinct()
|
||||
.where(
|
||||
(sr_country.country == country)
|
||||
& (sr.disabled != 1)
|
||||
)
|
||||
)
|
||||
result = query.run(as_list=True)
|
||||
shipping_rules = [x[0] for x in result]
|
||||
|
||||
return shipping_rules
|
||||
|
@ -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
|
||||
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.shopping_cart.cart import _get_cart_quotation, _set_price_list
|
||||
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
|
||||
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||
get_shopping_cart_settings,
|
||||
show_quantity_in_website,
|
||||
)
|
||||
from erpnext.utilities.product import get_non_stock_item_status, get_price, get_qty_in_stock
|
||||
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)
|
||||
@ -18,7 +21,11 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False):
|
||||
|
||||
cart_settings = get_shopping_cart_settings()
|
||||
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()
|
||||
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)
|
||||
|
||||
price = get_price(
|
||||
item_code,
|
||||
selling_price_list,
|
||||
cart_settings.default_customer_group,
|
||||
cart_settings.company
|
||||
)
|
||||
price = {}
|
||||
if cart_settings.show_price:
|
||||
is_guest = frappe.session.user == "Guest"
|
||||
# Show Price if logged in.
|
||||
# If not logged in, check if price is hidden for guest.
|
||||
if not is_guest or not cart_settings.hide_price_for_guest:
|
||||
price = get_price(
|
||||
item_code,
|
||||
selling_price_list,
|
||||
cart_settings.default_customer_group,
|
||||
cart_settings.company
|
||||
)
|
||||
|
||||
stock_status = get_qty_in_stock(item_code, "website_warehouse")
|
||||
stock_status = None
|
||||
|
||||
if cart_settings.show_stock_availability:
|
||||
on_backorder = frappe.get_cached_value("Website Item", {"item_code": item_code}, "on_backorder")
|
||||
if on_backorder:
|
||||
stock_status = frappe._dict({"on_backorder": True})
|
||||
else:
|
||||
stock_status = get_web_item_qty_in_stock(item_code, "website_warehouse")
|
||||
|
||||
product_info = {
|
||||
"price": price,
|
||||
"stock_qty": stock_status.stock_qty,
|
||||
"in_stock": stock_status.in_stock if stock_status.is_stock_item else get_non_stock_item_status(item_code, "website_warehouse"),
|
||||
"qty": 0,
|
||||
"uom": frappe.db.get_value("Item", item_code, "stock_uom"),
|
||||
"show_stock_qty": show_quantity_in_website(),
|
||||
"sales_uom": frappe.db.get_value("Item", item_code, "sales_uom")
|
||||
}
|
||||
|
||||
if stock_status:
|
||||
if stock_status.on_backorder:
|
||||
product_info["on_backorder"] = True
|
||||
else:
|
||||
product_info["stock_qty"] = stock_status.stock_qty
|
||||
product_info["in_stock"] = stock_status.in_stock if stock_status.is_stock_item else get_non_stock_item_status(item_code, "website_warehouse")
|
||||
product_info["show_stock_qty"] = show_quantity_in_website()
|
||||
|
||||
if product_info["price"]:
|
||||
if frappe.session.user != "Guest":
|
||||
item = cart_quotation.get({"item_code": item_code}) if cart_quotation else None
|
@ -8,8 +8,14 @@ import frappe
|
||||
from frappe.utils import add_months, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule
|
||||
from erpnext.shopping_cart.cart import _get_cart_quotation, get_party, update_cart
|
||||
from erpnext.tests.utils import create_test_contact_and_address
|
||||
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||
from erpnext.e_commerce.shopping_cart.cart import (
|
||||
_get_cart_quotation,
|
||||
get_cart_quotation,
|
||||
get_party,
|
||||
update_cart,
|
||||
)
|
||||
from erpnext.tests.utils import change_settings, create_test_contact_and_address
|
||||
|
||||
# test_dependencies = ['Payment Terms Template']
|
||||
|
||||
@ -27,8 +33,14 @@ class TestShoppingCart(unittest.TestCase):
|
||||
frappe.set_user("Administrator")
|
||||
create_test_contact_and_address()
|
||||
self.enable_shopping_cart()
|
||||
if not frappe.db.exists("Website Item", {"item_code": "_Test Item"}):
|
||||
make_website_item(frappe.get_cached_doc("Item", "_Test Item"))
|
||||
|
||||
if not frappe.db.exists("Website Item", {"item_code": "_Test Item 2"}):
|
||||
make_website_item(frappe.get_cached_doc("Item", "_Test Item 2"))
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
frappe.set_user("Administrator")
|
||||
self.disable_shopping_cart()
|
||||
|
||||
@ -123,6 +135,43 @@ class TestShoppingCart(unittest.TestCase):
|
||||
|
||||
self.remove_test_quotation(quotation)
|
||||
|
||||
@change_settings("E Commerce Settings",{
|
||||
"company": "_Test Company",
|
||||
"enabled": 1,
|
||||
"default_customer_group": "_Test Customer Group",
|
||||
"price_list": "_Test Price List India",
|
||||
"show_price": 1
|
||||
})
|
||||
def test_add_item_variant_without_web_item_to_cart(self):
|
||||
"Test adding Variants having no Website Items in cart via Template Web Item."
|
||||
from erpnext.controllers.item_variant import create_variant
|
||||
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
template_item = make_item("Test-Tshirt-Temp", {
|
||||
"has_variant": 1,
|
||||
"variant_based_on": "Item Attribute",
|
||||
"attributes": [
|
||||
{"attribute": "Test Size"},
|
||||
{"attribute": "Test Colour"}
|
||||
]
|
||||
})
|
||||
variant = create_variant("Test-Tshirt-Temp", {
|
||||
"Test Size": "Small", "Test Colour": "Red"
|
||||
})
|
||||
variant.save()
|
||||
make_website_item(template_item) # publish template not variant
|
||||
|
||||
update_cart("Test-Tshirt-Temp-S-R", 1)
|
||||
|
||||
cart = get_cart_quotation() # test if cart page gets data without errors
|
||||
doc = cart.get("doc")
|
||||
|
||||
self.assertEqual(doc.get("items")[0].item_name, "Test-Tshirt-Temp-S-R")
|
||||
|
||||
# test if items are rendered without error
|
||||
frappe.render_template("templates/includes/cart/cart_items.html", cart)
|
||||
|
||||
def create_tax_rule(self):
|
||||
tax_rule = frappe.get_test_records("Tax Rule")[0]
|
||||
try:
|
||||
@ -166,7 +215,7 @@ class TestShoppingCart(unittest.TestCase):
|
||||
|
||||
# helper functions
|
||||
def enable_shopping_cart(self):
|
||||
settings = frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings")
|
||||
settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
|
||||
|
||||
settings.update({
|
||||
"enabled": 1,
|
||||
@ -196,7 +245,7 @@ class TestShoppingCart(unittest.TestCase):
|
||||
frappe.local.shopping_cart_settings = None
|
||||
|
||||
def disable_shopping_cart(self):
|
||||
settings = frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings")
|
||||
settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
|
||||
settings.enabled = 0
|
||||
settings.save()
|
||||
frappe.local.shopping_cart_settings = None
|
@ -1,10 +1,8 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
import frappe
|
||||
|
||||
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
|
||||
is_cart_enabled,
|
||||
)
|
||||
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import is_cart_enabled
|
||||
|
||||
|
||||
def show_cart_count():
|
||||
@ -23,7 +21,7 @@ def set_cart_count(login_manager):
|
||||
return
|
||||
|
||||
if show_cart_count():
|
||||
from erpnext.shopping_cart.cart import set_cart_count
|
||||
from erpnext.e_commerce.shopping_cart.cart import set_cart_count
|
||||
|
||||
# set_cart_count will try to fetch existing cart quotation
|
||||
# or create one if non existent (and create a customer too)
|
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')
|
||||
if val: return val
|
||||
|
||||
all_attribute_values = frappe.db.get_all('Item Attribute Value',
|
||||
all_attribute_values = frappe.get_all('Item Attribute Value',
|
||||
['attribute_value', 'idx', 'parent'], order_by='idx asc')
|
||||
|
||||
ordered_attribute_values_map = frappe._dict({})
|
||||
@ -57,22 +57,35 @@ class ItemVariantsCacheManager:
|
||||
def build_cache(self):
|
||||
parent_item_code = self.item_code
|
||||
|
||||
attributes = [a.attribute for a in frappe.db.get_all('Item Variant Attribute',
|
||||
{'parent': parent_item_code}, ['attribute'], order_by='idx asc')
|
||||
attributes = [
|
||||
a.attribute for a in frappe.get_all(
|
||||
'Item Variant Attribute',
|
||||
{'parent': parent_item_code},
|
||||
['attribute'],
|
||||
order_by='idx asc'
|
||||
)
|
||||
]
|
||||
|
||||
item_variants_data = frappe.db.get_all('Item Variant Attribute',
|
||||
{'variant_of': parent_item_code}, ['parent', 'attribute', 'attribute_value'],
|
||||
# join with Website Item
|
||||
item_variants_data = frappe.get_all(
|
||||
'Item Variant Attribute',
|
||||
{'variant_of': parent_item_code},
|
||||
['parent', 'attribute', 'attribute_value'],
|
||||
order_by='name',
|
||||
as_list=1
|
||||
)
|
||||
|
||||
disabled_items = set([i.name for i in frappe.db.get_all('Item', {'disabled': 1})])
|
||||
disabled_items = set(
|
||||
[i.name for i in frappe.db.get_all('Item', {'disabled': 1})]
|
||||
)
|
||||
|
||||
attribute_value_item_map = frappe._dict({})
|
||||
item_attribute_value_map = frappe._dict({})
|
||||
attribute_value_item_map = frappe._dict()
|
||||
item_attribute_value_map = frappe._dict()
|
||||
|
||||
# dont consider variants that are disabled
|
||||
# pull all other variants
|
||||
item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items]
|
||||
|
||||
for row in item_variants_data:
|
||||
item_code, attribute, attribute_value = row
|
||||
# (attr, value) => [item1, item2]
|
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",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Template",
|
||||
@ -273,9 +274,9 @@
|
||||
}
|
||||
],
|
||||
"idx": 2,
|
||||
"modified": "2020-12-29 12:30:02.794994",
|
||||
"modified": "2021-02-24 15:57:05.889709",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Shopping Cart",
|
||||
"module": "E-commerce",
|
||||
"name": "Hero Slider",
|
||||
"owner": "Administrator",
|
||||
"standard": 1,
|
@ -23,11 +23,10 @@
|
||||
{%- for index in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] -%}
|
||||
{%- set item = values['card_' + index + '_item'] -%}
|
||||
{%- if item -%}
|
||||
{%- set item = frappe.get_doc("Item", item) -%}
|
||||
{%- set web_item = frappe.get_doc("Website Item", item) -%}
|
||||
{{ item_card(
|
||||
item.item_name, item.image, item.route, item.description,
|
||||
None, item.item_group, values['card_' + index + '_featured'],
|
||||
True, "Center"
|
||||
web_item, is_featured=values['card_' + index + '_featured'],
|
||||
is_full_width=True, align="Center"
|
||||
) }}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
@ -17,15 +17,12 @@
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"__unsaved": 1,
|
||||
"fieldname": "primary_action_label",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action Label",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"__islocal": 1,
|
||||
"__unsaved": 1,
|
||||
"fieldname": "primary_action",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action",
|
||||
@ -40,8 +37,8 @@
|
||||
{
|
||||
"fieldname": "card_1_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@ -59,8 +56,8 @@
|
||||
{
|
||||
"fieldname": "card_2_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@ -79,8 +76,8 @@
|
||||
{
|
||||
"fieldname": "card_3_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@ -98,8 +95,8 @@
|
||||
{
|
||||
"fieldname": "card_4_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@ -117,8 +114,8 @@
|
||||
{
|
||||
"fieldname": "card_5_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@ -136,8 +133,8 @@
|
||||
{
|
||||
"fieldname": "card_6_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@ -155,8 +152,8 @@
|
||||
{
|
||||
"fieldname": "card_7_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@ -174,8 +171,8 @@
|
||||
{
|
||||
"fieldname": "card_8_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@ -193,8 +190,8 @@
|
||||
{
|
||||
"fieldname": "card_9_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@ -212,8 +209,8 @@
|
||||
{
|
||||
"fieldname": "card_10_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@ -231,8 +228,8 @@
|
||||
{
|
||||
"fieldname": "card_11_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@ -250,8 +247,8 @@
|
||||
{
|
||||
"fieldname": "card_12_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@ -262,9 +259,9 @@
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"modified": "2020-11-19 18:48:52.633045",
|
||||
"modified": "2021-12-21 14:44:59.821335",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Shopping Cart",
|
||||
"module": "E-commerce",
|
||||
"name": "Item Card Group",
|
||||
"owner": "Administrator",
|
||||
"standard": 1,
|
@ -5,7 +5,6 @@
|
||||
"doctype": "Web Template",
|
||||
"fields": [
|
||||
{
|
||||
"__unsaved": 1,
|
||||
"fieldname": "item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
@ -13,7 +12,6 @@
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"__unsaved": 1,
|
||||
"fieldname": "featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
@ -22,9 +20,9 @@
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"modified": "2020-11-17 15:33:34.982515",
|
||||
"modified": "2021-02-24 16:05:17.926610",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Shopping Cart",
|
||||
"module": "E-commerce",
|
||||
"name": "Product Card",
|
||||
"owner": "Administrator",
|
||||
"standard": 1,
|
@ -6,8 +6,15 @@
|
||||
}) -%}
|
||||
<div class="card h-100">
|
||||
{% if image %}
|
||||
<img class="card-img-top" src="{{ image }}" alt="{{ title }}">
|
||||
<img class="card-img-top" src="{{ image }}" alt="{{ title }}" style="max-height: 200px;">
|
||||
{% else %}
|
||||
<div class="placeholder-div" style="max-height: 200px;">
|
||||
<span class="placeholder">
|
||||
{{ frappe.utils.get_abbr(title or '') }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card-body text-center text-muted small">
|
||||
{{ title or '' }}
|
||||
</div>
|
@ -74,9 +74,9 @@
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"modified": "2020-11-18 17:26:28.726260",
|
||||
"modified": "2021-02-24 16:03:33.835635",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Shopping Cart",
|
||||
"module": "E-commerce",
|
||||
"name": "Product Category Cards",
|
||||
"owner": "Administrator",
|
||||
"standard": 1,
|
@ -149,7 +149,6 @@ def create_item_code(amazon_item_json, sku):
|
||||
item.description = amazon_item_json.Product.AttributeSets.ItemAttributes.Title
|
||||
item.brand = new_brand
|
||||
item.manufacturer = new_manufacturer
|
||||
item.web_long_description = amazon_item_json.Product.AttributeSets.ItemAttributes.Title
|
||||
|
||||
item.image = amazon_item_json.Product.AttributeSets.ItemAttributes.SmallImage.URL
|
||||
|
||||
|
@ -51,15 +51,15 @@ additional_print_settings = "erpnext.controllers.print_settings.get_print_settin
|
||||
|
||||
on_session_creation = [
|
||||
"erpnext.portal.utils.create_customer_or_supplier",
|
||||
"erpnext.shopping_cart.utils.set_cart_count"
|
||||
"erpnext.e_commerce.shopping_cart.utils.set_cart_count"
|
||||
]
|
||||
on_logout = "erpnext.shopping_cart.utils.clear_cart_count"
|
||||
on_logout = "erpnext.e_commerce.shopping_cart.utils.clear_cart_count"
|
||||
|
||||
treeviews = ['Account', 'Cost Center', 'Warehouse', 'Item Group', 'Customer Group', 'Sales Person', 'Territory', 'Assessment Group', 'Department']
|
||||
|
||||
# website
|
||||
update_website_context = ["erpnext.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_context"]
|
||||
my_account_context = "erpnext.shopping_cart.utils.update_my_account_context"
|
||||
update_website_context = ["erpnext.e_commerce.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_context"]
|
||||
my_account_context = "erpnext.e_commerce.shopping_cart.utils.update_my_account_context"
|
||||
webform_list_context = "erpnext.controllers.website_list_for_contact.get_webform_list_context"
|
||||
|
||||
calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "Course Schedule"]
|
||||
@ -73,7 +73,7 @@ domains = {
|
||||
'Services': 'erpnext.domains.services',
|
||||
}
|
||||
|
||||
website_generators = ["Item Group", "Item", "BOM", "Sales Partner",
|
||||
website_generators = ["Item Group", "Website Item", "BOM", "Sales Partner",
|
||||
"Job Opening", "Student Admission"]
|
||||
|
||||
website_context = {
|
||||
@ -237,10 +237,7 @@ doc_events = {
|
||||
]
|
||||
},
|
||||
"Sales Taxes and Charges Template": {
|
||||
"on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings"
|
||||
},
|
||||
"Website Settings": {
|
||||
"validate": "erpnext.portal.doctype.products_settings.products_settings.home_page_is_products"
|
||||
"on_update": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.validate_cart_settings"
|
||||
},
|
||||
"Tax Category": {
|
||||
"validate": "erpnext.regional.india.utils.validate_tax_category"
|
||||
|
@ -9,7 +9,6 @@ Manufacturing
|
||||
Stock
|
||||
Support
|
||||
Utilities
|
||||
Shopping Cart
|
||||
Assets
|
||||
Portal
|
||||
Maintenance
|
||||
@ -22,3 +21,4 @@ Communication
|
||||
Loan Management
|
||||
Payroll
|
||||
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.create_gst_payment_entry_fields #27-11-2021
|
||||
erpnext.patches.v13_0.fix_invoice_statuses
|
||||
erpnext.patches.v13_0.create_website_items #30-09-2021
|
||||
erpnext.patches.v13_0.populate_e_commerce_settings
|
||||
erpnext.patches.v13_0.make_homepage_products_website_items
|
||||
erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item
|
||||
erpnext.patches.v13_0.update_dates_in_tax_withholding_category
|
||||
erpnext.patches.v14_0.update_opportunity_currency_fields
|
||||
@ -314,6 +317,7 @@ erpnext.patches.v13_0.item_naming_series_not_mandatory
|
||||
erpnext.patches.v14_0.delete_healthcare_doctypes
|
||||
erpnext.patches.v13_0.update_category_in_ltds_certificate
|
||||
erpnext.patches.v13_0.create_pan_field_for_india #2
|
||||
erpnext.patches.v13_0.fetch_thumbnail_in_website_items
|
||||
erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit
|
||||
erpnext.patches.v13_0.create_ksa_vat_custom_fields # 07-01-2022
|
||||
erpnext.patches.v14_0.migrate_crm_settings
|
||||
@ -343,3 +347,5 @@ erpnext.patches.v14_0.restore_einvoice_fields
|
||||
erpnext.patches.v13_0.update_sane_transfer_against
|
||||
erpnext.patches.v12_0.add_company_link_to_einvoice_settings
|
||||
erpnext.patches.v14_0.migrate_cost_center_allocations
|
||||
erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template
|
||||
erpnext.patches.v13_0.shopping_cart_to_ecommerce
|
||||
|
@ -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', {
|
||||
setup: function(frm) {
|
||||
frm.fields_dict["products"].grid.get_field("item_code").get_query = function(){
|
||||
frm.fields_dict["products"].grid.get_field("item").get_query = function() {
|
||||
return {
|
||||
filters: {'show_in_website': 1}
|
||||
filters: {'published': 1}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -21,11 +21,10 @@ frappe.ui.form.on('Homepage', {
|
||||
});
|
||||
|
||||
frappe.ui.form.on('Homepage Featured Product', {
|
||||
|
||||
view: function(frm, cdt, cdn){
|
||||
var child= locals[cdt][cdn]
|
||||
if(child.item_code && frm.doc.products_url){
|
||||
window.location.href = frm.doc.products_url + '/' + encodeURIComponent(child.item_code);
|
||||
view: function(frm, cdt, cdn) {
|
||||
var child= locals[cdt][cdn];
|
||||
if (child.item_code && child.route) {
|
||||
window.open('/' + child.route, '_blank');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1,518 +1,143 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"autoname": "",
|
||||
"actions": [],
|
||||
"beta": 1,
|
||||
"creation": "2016-04-22 05:27:52.109319",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 0,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"hero_section_based_on",
|
||||
"column_break_2",
|
||||
"title",
|
||||
"section_break_4",
|
||||
"tag_line",
|
||||
"description",
|
||||
"hero_image",
|
||||
"slideshow",
|
||||
"hero_section",
|
||||
"products_section",
|
||||
"products_url",
|
||||
"products"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Company",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Company",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "hero_section_based_on",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Hero Section Based On",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Default\nSlideshow\nHomepage Section",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"options": "Default\nSlideshow\nHomepage Section"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "",
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Title",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Title"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "",
|
||||
"fieldname": "section_break_4",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Hero Section",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Hero Section"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "eval:doc.hero_section_based_on === 'Default'",
|
||||
"description": "Company Tagline for website homepage",
|
||||
"fieldname": "tag_line",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Tag Line",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "eval:doc.hero_section_based_on === 'Default'",
|
||||
"description": "Company Description for website homepage",
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Description",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "eval:doc.hero_section_based_on === 'Default'",
|
||||
"fieldname": "hero_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Hero Image",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Hero Image"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "eval:doc.hero_section_based_on === 'Slideshow'",
|
||||
"description": "",
|
||||
"fieldname": "slideshow",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Homepage Slideshow",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Website Slideshow",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"options": "Website Slideshow"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "eval:doc.hero_section_based_on === 'Homepage Section'",
|
||||
"fieldname": "hero_section",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Homepage Section",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Homepage Section",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"options": "Homepage Section"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "",
|
||||
"fieldname": "products_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Products",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Products"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "/products",
|
||||
"default": "/all-products",
|
||||
"fieldname": "products_url",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "URL for \"All Products\"",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "URL for \"All Products\""
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "Products to be shown on website homepage",
|
||||
"fieldname": "products",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Products",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Homepage Featured Product",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": "40px"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 1,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-03-02 23:12:59.676202",
|
||||
"links": [],
|
||||
"modified": "2021-02-18 13:29:29.531639",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Portal",
|
||||
"name": "Homepage",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 0,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 0,
|
||||
"role": "Administrator",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "company",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
"track_changes": 1
|
||||
}
|
@ -14,12 +14,14 @@ class Homepage(Document):
|
||||
delete_page_cache('home')
|
||||
|
||||
def setup_items(self):
|
||||
for d in frappe.get_all('Item', fields=['name', 'item_name', 'description', 'image'],
|
||||
filters={'show_in_website': 1}, limit=3):
|
||||
for d in frappe.get_all('Website Item', fields=['name', 'item_name', 'description', 'image', 'route'],
|
||||
filters={'published': 1}, limit=3):
|
||||
|
||||
doc = frappe.get_doc('Item', d.name)
|
||||
doc = frappe.get_doc('Website Item', d.name)
|
||||
if not doc.route:
|
||||
# set missing route
|
||||
doc.save()
|
||||
self.append('products', dict(item_code=d.name,
|
||||
item_name=d.item_name, description=d.description, image=d.image))
|
||||
item_name=d.item_name, description=d.description,
|
||||
image=d.image, route=d.route))
|
||||
|
||||
|
@ -25,10 +25,10 @@
|
||||
"fieldtype": "Link",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Item Code",
|
||||
"label": "Item",
|
||||
"oldfieldname": "item_code",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Item",
|
||||
"options": "Website Item",
|
||||
"print_width": "150px",
|
||||
"reqd": 1,
|
||||
"search_index": 1,
|
||||
@ -63,7 +63,7 @@
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Description"
|
||||
"label": "Details"
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.web_long_description",
|
||||
@ -89,12 +89,14 @@
|
||||
"label": "Image"
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.thumbnail",
|
||||
"fieldname": "thumbnail",
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 1,
|
||||
"label": "Thumbnail"
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.route",
|
||||
"fieldname": "route",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "route",
|
||||
@ -104,7 +106,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-08-25 15:27:49.573537",
|
||||
"modified": "2021-02-18 13:05:50.669311",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Portal",
|
||||
"name": "Homepage Featured Product",
|
||||
|
@ -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,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"actions": [],
|
||||
"creation": "2019-01-01 13:04:54.479079",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"attribute"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "attribute",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Attribute",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Item Attribute",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-01-01 13:04:59.715572",
|
||||
"links": [],
|
||||
"modified": "2021-02-18 13:18:57.810536",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Portal",
|
||||
"name": "Website Attribute",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
"track_changes": 1
|
||||
}
|
@ -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
|
||||
from frappe.utils.nestedset import get_root_of
|
||||
|
||||
from erpnext.shopping_cart.cart import get_debtors_account
|
||||
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
|
||||
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||
get_shopping_cart_settings,
|
||||
)
|
||||
from erpnext.e_commerce.shopping_cart.cart import get_debtors_account
|
||||
|
||||
|
||||
def set_default_role(doc, method):
|
||||
|
@ -7,7 +7,8 @@
|
||||
],
|
||||
"js/erpnext-web.min.js": [
|
||||
"public/js/website_utils.js",
|
||||
"public/js/shopping_cart.js"
|
||||
"public/js/shopping_cart.js",
|
||||
"public/js/wishlist.js"
|
||||
],
|
||||
"css/erpnext-web.css": [
|
||||
"public/scss/website.scss",
|
||||
@ -65,5 +66,11 @@
|
||||
"js/hierarchy-chart.min.js": [
|
||||
"public/js/hierarchy_chart/hierarchy_chart_desktop.js",
|
||||
"public/js/hierarchy_chart/hierarchy_chart_mobile.js"
|
||||
],
|
||||
"js/e-commerce.min.js": [
|
||||
"e_commerce/product_ui/views.js",
|
||||
"e_commerce/product_ui/grid.js",
|
||||
"e_commerce/product_ui/list.js",
|
||||
"e_commerce/product_ui/search.js"
|
||||
]
|
||||
}
|
||||
|
@ -4,8 +4,8 @@
|
||||
// js inside blog page
|
||||
|
||||
// shopping cart
|
||||
frappe.provide("erpnext.shopping_cart");
|
||||
var shopping_cart = erpnext.shopping_cart;
|
||||
frappe.provide("erpnext.e_commerce.shopping_cart");
|
||||
var shopping_cart = erpnext.e_commerce.shopping_cart;
|
||||
|
||||
$.extend(shopping_cart, {
|
||||
show_error: function(title, text) {
|
||||
@ -18,8 +18,8 @@ $.extend(shopping_cart, {
|
||||
shopping_cart.bind_place_order();
|
||||
shopping_cart.bind_request_quotation();
|
||||
shopping_cart.bind_change_qty();
|
||||
shopping_cart.bind_remove_cart_item();
|
||||
shopping_cart.bind_change_notes();
|
||||
shopping_cart.bind_dropdown_cart_buttons();
|
||||
shopping_cart.bind_coupon_code();
|
||||
},
|
||||
|
||||
@ -48,7 +48,7 @@ $.extend(shopping_cart, {
|
||||
const address_name = $card.closest('[data-address-name]').attr('data-address-name');
|
||||
frappe.call({
|
||||
type: "POST",
|
||||
method: "erpnext.shopping_cart.cart.update_cart_address",
|
||||
method: "erpnext.e_commerce.shopping_cart.cart.update_cart_address",
|
||||
freeze: true,
|
||||
args: {
|
||||
address_type,
|
||||
@ -57,7 +57,7 @@ $.extend(shopping_cart, {
|
||||
callback: function(r) {
|
||||
d.hide();
|
||||
if (!r.exc) {
|
||||
$(".cart-tax-items").html(r.message.taxes);
|
||||
$(".cart-tax-items").html(r.message.total);
|
||||
shopping_cart.parent.find(
|
||||
`.address-container[data-address-type="${address_type}"]`
|
||||
).html(r.message.address);
|
||||
@ -129,8 +129,14 @@ $.extend(shopping_cart, {
|
||||
}
|
||||
}
|
||||
input.val(newVal);
|
||||
|
||||
let notes = input.closest("td").siblings().find(".notes").text().trim();
|
||||
var item_code = input.attr("data-item-code");
|
||||
shopping_cart.shopping_cart_update({item_code, qty: newVal});
|
||||
shopping_cart.shopping_cart_update({
|
||||
item_code,
|
||||
qty: newVal,
|
||||
additional_notes: notes
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@ -148,6 +154,18 @@ $.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) {
|
||||
var shipping_selector;
|
||||
if(shipping_rules) {
|
||||
@ -185,7 +203,7 @@ $.extend(shopping_cart, {
|
||||
return frappe.call({
|
||||
btn: btn,
|
||||
type: "POST",
|
||||
method: "erpnext.shopping_cart.cart.apply_shipping_rule",
|
||||
method: "erpnext.e_commerce.shopping_cart.cart.apply_shipping_rule",
|
||||
args: { shipping_rule: rule },
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
@ -196,12 +214,15 @@ $.extend(shopping_cart, {
|
||||
},
|
||||
|
||||
place_order: function(btn) {
|
||||
shopping_cart.freeze();
|
||||
|
||||
return frappe.call({
|
||||
type: "POST",
|
||||
method: "erpnext.shopping_cart.cart.place_order",
|
||||
method: "erpnext.e_commerce.shopping_cart.cart.place_order",
|
||||
btn: btn,
|
||||
callback: function(r) {
|
||||
if(r.exc) {
|
||||
shopping_cart.unfreeze();
|
||||
var msg = "";
|
||||
if(r._server_messages) {
|
||||
msg = JSON.parse(r._server_messages || []).join("<br>");
|
||||
@ -212,7 +233,6 @@ $.extend(shopping_cart, {
|
||||
.html(msg || frappe._("Something went wrong!"))
|
||||
.toggle(true);
|
||||
} else {
|
||||
$('.cart-container table').hide();
|
||||
$(btn).hide();
|
||||
window.location.href = '/orders/' + encodeURIComponent(r.message);
|
||||
}
|
||||
@ -221,12 +241,15 @@ $.extend(shopping_cart, {
|
||||
},
|
||||
|
||||
request_quotation: function(btn) {
|
||||
shopping_cart.freeze();
|
||||
|
||||
return frappe.call({
|
||||
type: "POST",
|
||||
method: "erpnext.shopping_cart.cart.request_for_quotation",
|
||||
method: "erpnext.e_commerce.shopping_cart.cart.request_for_quotation",
|
||||
btn: btn,
|
||||
callback: function(r) {
|
||||
if(r.exc) {
|
||||
shopping_cart.unfreeze();
|
||||
var msg = "";
|
||||
if(r._server_messages) {
|
||||
msg = JSON.parse(r._server_messages || []).join("<br>");
|
||||
@ -237,7 +260,6 @@ $.extend(shopping_cart, {
|
||||
.html(msg || frappe._("Something went wrong!"))
|
||||
.toggle(true);
|
||||
} else {
|
||||
$('.cart-container table').hide();
|
||||
$(btn).hide();
|
||||
window.location.href = '/quotations/' + encodeURIComponent(r.message);
|
||||
}
|
||||
@ -254,7 +276,7 @@ $.extend(shopping_cart, {
|
||||
apply_coupon_code: function(btn) {
|
||||
return frappe.call({
|
||||
type: "POST",
|
||||
method: "erpnext.shopping_cart.cart.apply_coupon_code",
|
||||
method: "erpnext.e_commerce.shopping_cart.cart.apply_coupon_code",
|
||||
btn: btn,
|
||||
args : {
|
||||
applied_code : $('.txtcoupon').val(),
|
||||
@ -270,7 +292,9 @@ $.extend(shopping_cart, {
|
||||
});
|
||||
|
||||
frappe.ready(function() {
|
||||
$(".cart-icon").hide();
|
||||
if (window.location.pathname === "/cart") {
|
||||
$(".cart-icon").hide();
|
||||
}
|
||||
shopping_cart.parent = $(".cart-container");
|
||||
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