mirror of
https://github.com/frappe/erpnext.git
synced 2026-02-18 17:15:04 +00:00
Merge branch 'develop' into e_com_perms
This commit is contained in:
@@ -1,13 +1,9 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt, get_url, nowdate
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -363,33 +359,6 @@ class PaymentRequest(Document):
|
||||
def get_payment_success_url(self):
|
||||
return self.payment_success_url
|
||||
|
||||
def on_payment_authorized(self, status=None):
|
||||
if not status:
|
||||
return
|
||||
|
||||
shopping_cart_settings = frappe.get_doc("E Commerce Settings")
|
||||
|
||||
if status in ["Authorized", "Completed"]:
|
||||
redirect_to = None
|
||||
self.set_as_paid()
|
||||
|
||||
# if shopping cart enabled and in session
|
||||
if (
|
||||
shopping_cart_settings.enabled
|
||||
and hasattr(frappe.local, "session")
|
||||
and frappe.local.session.user != "Guest"
|
||||
) and self.payment_channel != "Phone":
|
||||
|
||||
success_url = shopping_cart_settings.payment_success_url
|
||||
if success_url:
|
||||
redirect_to = ({"Orders": "/orders", "Invoices": "/invoices", "My Account": "/me"}).get(
|
||||
success_url, "/me"
|
||||
)
|
||||
else:
|
||||
redirect_to = get_url("/orders/{0}".format(self.reference_name))
|
||||
|
||||
return redirect_to
|
||||
|
||||
def create_subscription(self, payment_provider, gateway_controller, data):
|
||||
if payment_provider == "stripe":
|
||||
with payment_app_import_guard():
|
||||
@@ -546,13 +515,12 @@ def get_existing_payment_request_amount(ref_dt, ref_dn):
|
||||
|
||||
|
||||
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("E Commerce Settings").payment_gateway_account
|
||||
return get_payment_gateway_account(payment_gateway_account)
|
||||
"""
|
||||
Return gateway and payment account of default payment gateway
|
||||
"""
|
||||
gateway_account = args.get("payment_gateway_account", {"is_default": 1})
|
||||
if gateway_account:
|
||||
return get_payment_gateway_account(gateway_account)
|
||||
|
||||
gateway_account = get_payment_gateway_account({"is_default": 1})
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class SubscriptionPlan(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_plan_rate(
|
||||
plan, quantity=1, customer=None, start_date=None, end_date=None, prorate_factor=1
|
||||
plan, quantity=1, customer=None, start_date=None, end_date=None, prorate_factor=1, party=None
|
||||
):
|
||||
plan = frappe.get_doc("Subscription Plan", plan)
|
||||
if plan.price_determination == "Fixed Rate":
|
||||
@@ -40,6 +40,7 @@ def get_plan_rate(
|
||||
customer_group=customer_group,
|
||||
company=None,
|
||||
qty=quantity,
|
||||
party=party,
|
||||
)
|
||||
if not price:
|
||||
return 0
|
||||
|
||||
@@ -8,7 +8,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.doctype.address.address import get_default_address
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, cstr
|
||||
from frappe.utils import cstr
|
||||
from frappe.utils.nestedset import get_root_of
|
||||
|
||||
from erpnext.setup.doctype.customer_group.customer_group import get_parent_customer_groups
|
||||
@@ -34,7 +34,6 @@ class TaxRule(Document):
|
||||
self.validate_tax_template()
|
||||
self.validate_from_to_dates("from_date", "to_date")
|
||||
self.validate_filters()
|
||||
self.validate_use_for_shopping_cart()
|
||||
|
||||
def validate_tax_template(self):
|
||||
if self.tax_type == "Sales":
|
||||
@@ -106,21 +105,6 @@ class TaxRule(Document):
|
||||
if tax_rule[0].priority == self.priority:
|
||||
frappe.throw(_("Tax Rule Conflicts with {0}").format(tax_rule[0].name), ConflictingTaxRule)
|
||||
|
||||
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("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
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Enabling 'Use for Shopping Cart', as Shopping Cart is enabled and there should be at least one Tax Rule for Shopping Cart"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_party_details(party, party_type, args=None):
|
||||
|
||||
@@ -9,6 +9,8 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cstr, flt
|
||||
|
||||
from erpnext.utilities.product import get_item_codes_by_attributes
|
||||
|
||||
|
||||
class ItemVariantExistsError(frappe.ValidationError):
|
||||
pass
|
||||
@@ -24,7 +26,8 @@ class ItemTemplateCannotHaveStock(frappe.ValidationError):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_variant(template, args=None, variant=None, manufacturer=None, manufacturer_part_no=None):
|
||||
"""Validates Attributes and their Values, then looks for an exactly
|
||||
"""
|
||||
Validates Attributes and their Values, then looks for an exactly
|
||||
matching Item Variant
|
||||
|
||||
:param item: Template Item
|
||||
@@ -34,13 +37,14 @@ def get_variant(template, args=None, variant=None, manufacturer=None, manufactur
|
||||
|
||||
if item_template.variant_based_on == "Manufacturer" and manufacturer:
|
||||
return make_variant_based_on_manufacturer(item_template, manufacturer, manufacturer_part_no)
|
||||
else:
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
if not args:
|
||||
frappe.throw(_("Please specify at least one attribute in the Attributes table"))
|
||||
return find_variant(template, args, variant)
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
if not args:
|
||||
frappe.throw(_("Please specify at least one attribute in the Attributes table"))
|
||||
|
||||
return find_variant(template, args, variant)
|
||||
|
||||
|
||||
def make_variant_based_on_manufacturer(template, manufacturer, manufacturer_part_no):
|
||||
@@ -157,17 +161,6 @@ def get_attribute_values(item):
|
||||
|
||||
|
||||
def find_variant(template, args, variant_item_code=None):
|
||||
conditions = [
|
||||
"""(iv_attribute.attribute={0} and iv_attribute.attribute_value={1})""".format(
|
||||
frappe.db.escape(key), frappe.db.escape(cstr(value))
|
||||
)
|
||||
for key, value in args.items()
|
||||
]
|
||||
|
||||
conditions = " or ".join(conditions)
|
||||
|
||||
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
|
||||
]
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
# -*- 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:
|
||||
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:
|
||||
frappe.log_error("Product query with filter failed")
|
||||
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,58 +0,0 @@
|
||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("E Commerce Settings", {
|
||||
onload: function(frm) {
|
||||
if(frm.doc.__onload && frm.doc.__onload.quotation_series) {
|
||||
frm.fields_dict.quotation_series.df.options = frm.doc.__onload.quotation_series;
|
||||
frm.refresh_field("quotation_series");
|
||||
}
|
||||
|
||||
frm.set_query('payment_gateway_account', function() {
|
||||
return { 'filters': { 'payment_channel': "Email" } };
|
||||
});
|
||||
},
|
||||
refresh: function(frm) {
|
||||
if (frm.doc.enabled) {
|
||||
frm.get_field('store_page_docs').$wrapper.removeClass('hide-control').html(
|
||||
`<div>${__("Follow these steps to create a landing page for your store")}:
|
||||
<a href="https://docs.erpnext.com/docs/user/manual/en/website/store-landing-page"
|
||||
style="color: var(--gray-600)">
|
||||
docs/store-landing-page
|
||||
</a>
|
||||
</div>`
|
||||
);
|
||||
}
|
||||
|
||||
frappe.model.with_doctype("Website 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.get_field("filter_fields").grid.update_docfield_property(
|
||||
'fieldname', 'options', valid_fields
|
||||
);
|
||||
});
|
||||
},
|
||||
enabled: function(frm) {
|
||||
if (frm.doc.enabled === 1) {
|
||||
frm.set_value('enable_variants', 1);
|
||||
}
|
||||
else {
|
||||
frm.set_value('company', '');
|
||||
frm.set_value('price_list', '');
|
||||
frm.set_value('default_customer_group', '');
|
||||
frm.set_value('quotation_series', '');
|
||||
}
|
||||
},
|
||||
|
||||
enable_checkout: function(frm) {
|
||||
if (frm.doc.enable_checkout) {
|
||||
erpnext.utils.check_payments_app();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,395 +0,0 @@
|
||||
{
|
||||
"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",
|
||||
"is_redisearch_enabled",
|
||||
"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",
|
||||
"mandatory_depends_on": "is_redisearch_enabled",
|
||||
"read_only_depends_on": "eval:!doc.is_redisearch_loaded"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "item_search_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Item Search Settings"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_redisearch_enabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Redisearch",
|
||||
"read_only_depends_on": "eval:!doc.is_redisearch_loaded"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-01 18:35:56.106756",
|
||||
"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",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import comma_and, flt, unique
|
||||
|
||||
from erpnext.e_commerce.redisearch_utils import (
|
||||
create_website_items_index,
|
||||
define_autocomplete_dictionary,
|
||||
get_indexable_web_fields,
|
||||
is_search_module_loaded,
|
||||
)
|
||||
|
||||
|
||||
class ShoppingCartSetupError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class ECommerceSettings(Document):
|
||||
def onload(self):
|
||||
self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series")
|
||||
|
||||
# flag >> if redisearch is installed and loaded
|
||||
self.is_redisearch_loaded = is_search_module_loaded()
|
||||
|
||||
def validate(self):
|
||||
self.validate_field_filters(self.filter_fields, self.enable_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")
|
||||
|
||||
self.is_redisearch_enabled_pre_save = frappe.db.get_single_value(
|
||||
"E Commerce Settings", "is_redisearch_enabled"
|
||||
)
|
||||
|
||||
def after_save(self):
|
||||
self.create_redisearch_indexes()
|
||||
|
||||
def create_redisearch_indexes(self):
|
||||
# if redisearch is enabled (value changed) create indexes and dictionary
|
||||
value_changed = self.is_redisearch_enabled != self.is_redisearch_enabled_pre_save
|
||||
if self.is_redisearch_loaded and self.is_redisearch_enabled and value_changed:
|
||||
define_autocomplete_dictionary()
|
||||
create_website_items_index()
|
||||
|
||||
@staticmethod
|
||||
def validate_field_filters(filter_fields, enable_field_filters):
|
||||
if not (enable_field_filters and filter_fields):
|
||||
return
|
||||
|
||||
web_item_meta = frappe.get_meta("Website Item")
|
||||
valid_fields = [
|
||||
df.fieldname for df in web_item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]
|
||||
]
|
||||
|
||||
for row in filter_fields:
|
||||
if row.fieldname not in valid_fields:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Filter Fields Row #{0}: Fieldname {1} must be of type 'Link' or 'Table MultiSelect'"
|
||||
).format(row.idx, frappe.bold(row.fieldname))
|
||||
)
|
||||
|
||||
def validate_attribute_filters(self):
|
||||
if not (self.enable_attribute_filters and self.filter_attributes):
|
||||
return
|
||||
|
||||
# if attribute filters are enabled, hide_variants should be disabled
|
||||
self.hide_variants = 0
|
||||
|
||||
def validate_checkout(self):
|
||||
if self.enable_checkout and not self.payment_gateway_account:
|
||||
self.enable_checkout = 0
|
||||
|
||||
def validate_search_index_fields(self):
|
||||
if not self.search_index_fields:
|
||||
return
|
||||
|
||||
fields = self.search_index_fields.replace(" ", "")
|
||||
fields = unique(fields.strip(",").split(",")) # Remove extra ',' and remove duplicates
|
||||
|
||||
# All fields should be indexable
|
||||
allowed_indexable_fields = get_indexable_web_fields()
|
||||
|
||||
if not (set(fields).issubset(allowed_indexable_fields)):
|
||||
invalid_fields = list(set(fields).difference(allowed_indexable_fields))
|
||||
num_invalid_fields = len(invalid_fields)
|
||||
invalid_fields = comma_and(invalid_fields)
|
||||
|
||||
if num_invalid_fields > 1:
|
||||
frappe.throw(
|
||||
_("{0} are not valid options for Search Index Field.").format(frappe.bold(invalid_fields))
|
||||
)
|
||||
else:
|
||||
frappe.throw(
|
||||
_("{0} is not a valid option for Search Index Field.").format(frappe.bold(invalid_fields))
|
||||
)
|
||||
|
||||
self.search_index_fields = ",".join(fields)
|
||||
|
||||
def validate_price_list_exchange_rate(self):
|
||||
"Check if exchange rate exists for Price List currency (to Company's currency)."
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
if not self.enabled or not self.company or not self.price_list:
|
||||
return # this function is also called from hooks, check values again
|
||||
|
||||
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
price_list_currency = frappe.db.get_value("Price List", self.price_list, "currency")
|
||||
|
||||
if not company_currency:
|
||||
msg = f"Please specify currency in Company {self.company}"
|
||||
frappe.throw(_(msg), title=_("Missing Currency"), exc=ShoppingCartSetupError)
|
||||
|
||||
if not price_list_currency:
|
||||
msg = f"Please specify currency in Price List {frappe.bold(self.price_list)}"
|
||||
frappe.throw(_(msg), title=_("Missing Currency"), exc=ShoppingCartSetupError)
|
||||
|
||||
if price_list_currency != company_currency:
|
||||
from_currency, to_currency = price_list_currency, company_currency
|
||||
|
||||
# Get exchange rate checks Currency Exchange Records too
|
||||
exchange_rate = get_exchange_rate(from_currency, to_currency, args="for_selling")
|
||||
|
||||
if not flt(exchange_rate):
|
||||
msg = f"Missing Currency Exchange Rates for {from_currency}-{to_currency}"
|
||||
frappe.throw(_(msg), title=_("Missing"), exc=ShoppingCartSetupError)
|
||||
|
||||
def validate_tax_rule(self):
|
||||
if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"):
|
||||
frappe.throw(frappe._("Set Tax Rule for shopping cart"), ShoppingCartSetupError)
|
||||
|
||||
def get_tax_master(self, billing_territory):
|
||||
tax_master = self.get_name_from_territory(
|
||||
billing_territory, "sales_taxes_and_charges_masters", "sales_taxes_and_charges_master"
|
||||
)
|
||||
return tax_master and tax_master[0] or None
|
||||
|
||||
def get_shipping_rules(self, shipping_territory):
|
||||
return self.get_name_from_territory(shipping_territory, "shipping_rules", "shipping_rule")
|
||||
|
||||
def on_change(self):
|
||||
old_doc = self.get_doc_before_save()
|
||||
|
||||
if old_doc:
|
||||
old_fields = old_doc.search_index_fields
|
||||
new_fields = self.search_index_fields
|
||||
|
||||
# if search index fields get changed
|
||||
if not (new_fields == old_fields):
|
||||
create_website_items_index()
|
||||
|
||||
|
||||
def validate_cart_settings(doc=None, method=None):
|
||||
frappe.get_doc("E Commerce Settings", "E Commerce Settings").run_method("validate")
|
||||
|
||||
|
||||
def get_shopping_cart_settings():
|
||||
return frappe.get_cached_doc("E Commerce Settings")
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def is_cart_enabled():
|
||||
return get_shopping_cart_settings().enabled
|
||||
|
||||
|
||||
def show_quantity_in_website():
|
||||
return get_shopping_cart_settings().show_quantity_in_website
|
||||
|
||||
|
||||
def check_shopping_cart_enabled():
|
||||
if not get_shopping_cart_settings().enabled:
|
||||
frappe.throw(_("You need to enable Shopping Cart"), ShoppingCartSetupError)
|
||||
|
||||
|
||||
def show_attachments():
|
||||
return get_shopping_cart_settings().show_attachments
|
||||
@@ -1,53 +0,0 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||
ShoppingCartSetupError,
|
||||
)
|
||||
|
||||
|
||||
class TestECommerceSettings(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_tax_rule_validation(self):
|
||||
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
|
||||
frappe.db.commit() # nosemgrep
|
||||
|
||||
cart_settings = frappe.get_doc("E Commerce Settings")
|
||||
cart_settings.enabled = 1
|
||||
if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"):
|
||||
self.assertRaises(ShoppingCartSetupError, cart_settings.validate_tax_rule)
|
||||
|
||||
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1")
|
||||
|
||||
def test_invalid_filter_fields(self):
|
||||
"Check if Item fields are blocked in E Commerce Settings filter fields."
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
|
||||
setup_e_commerce_settings({"enable_field_filters": 1})
|
||||
|
||||
create_custom_field(
|
||||
"Item",
|
||||
dict(owner="Administrator", fieldname="test_data", label="Test", fieldtype="Data"),
|
||||
)
|
||||
settings = frappe.get_doc("E Commerce Settings")
|
||||
settings.append("filter_fields", {"fieldname": "test_data"})
|
||||
|
||||
self.assertRaises(frappe.ValidationError, settings.save)
|
||||
|
||||
|
||||
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"]
|
||||
@@ -1,8 +0,0 @@
|
||||
// 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) {
|
||||
|
||||
// }
|
||||
});
|
||||
@@ -1,134 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
# -*- 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
|
||||
)
|
||||
@@ -1,84 +0,0 @@
|
||||
# -*- 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")
|
||||
@@ -1,88 +0,0 @@
|
||||
{
|
||||
"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.website_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": "2022-06-28 16:44:24.718728",
|
||||
"modified_by": "Administrator",
|
||||
"module": "E-commerce",
|
||||
"name": "Recommended Items",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# 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
|
||||
@@ -1,7 +0,0 @@
|
||||
{% extends "templates/web.html" %}
|
||||
|
||||
{% block page_content %}
|
||||
<h1>{{ title }}</h1>
|
||||
{% endblock %}
|
||||
|
||||
<!-- this is a sample default web page template -->
|
||||
@@ -1,4 +0,0 @@
|
||||
<div>
|
||||
<a href="{{ doc.route }}">{{ doc.title or doc.name }}</a>
|
||||
</div>
|
||||
<!-- this is a sample default list template -->
|
||||
@@ -1,564 +0,0 @@
|
||||
# -*- 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)
|
||||
|
||||
settings = frappe.get_cached_doc("E Commerce Settings")
|
||||
if settings.enable_field_filters:
|
||||
base_breadcrumb = "Shop by Category"
|
||||
else:
|
||||
base_breadcrumb = "All Products"
|
||||
|
||||
self.assertEqual(breadcrumbs[0]["name"], "Home")
|
||||
self.assertEqual(breadcrumbs[1]["name"], base_breadcrumb)
|
||||
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.0)
|
||||
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.0%")
|
||||
|
||||
# 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)
|
||||
|
||||
# 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"], 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(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"]
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Website Item', {
|
||||
onload: (frm) => {
|
||||
// should never check Private
|
||||
frm.fields_dict["website_image"].df.is_private = 0;
|
||||
},
|
||||
|
||||
refresh: (frm) => {
|
||||
frm.add_custom_button(__("Prices"), function() {
|
||||
frappe.set_route("List", "Item Price", {"item_code": frm.doc.item_code});
|
||||
}, __("View"));
|
||||
|
||||
frm.add_custom_button(__("Stock"), function() {
|
||||
frappe.route_options = {
|
||||
"item_code": frm.doc.item_code
|
||||
};
|
||||
frappe.set_route("query-report", "Stock Balance");
|
||||
}, __("View"));
|
||||
|
||||
frm.add_custom_button(__("E Commerce Settings"), function() {
|
||||
frappe.set_route("Form", "E Commerce Settings");
|
||||
}, __("View"));
|
||||
},
|
||||
|
||||
copy_from_item_group: (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);
|
||||
}
|
||||
});
|
||||
@@ -1,414 +0,0 @@
|
||||
{
|
||||
"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",
|
||||
"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 Image",
|
||||
"hidden": 1,
|
||||
"in_preview": 1,
|
||||
"label": "Website Image",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"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. If the parent warehouse is selected, then the system will display the consolidated available quantity of all child warehouses.",
|
||||
"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,
|
||||
"search_index": 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",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"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": "website_image",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2023-09-12 14:19:22.822689",
|
||||
"modified_by": "Administrator",
|
||||
"module": "E-commerce",
|
||||
"name": "Website Item",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"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",
|
||||
"states": [],
|
||||
"title_field": "web_item_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,469 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING, List, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from erpnext.stock.doctype.item.item import Item
|
||||
|
||||
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 get_default_naming_series, make_autoname
|
||||
|
||||
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"""
|
||||
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:
|
||||
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:
|
||||
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
|
||||
|
||||
db_website_image = frappe.db.get_value(self.doctype, self.name, "website_image")
|
||||
if not self.is_new() and self.website_image != db_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
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def copy_specification_from_item_group(self):
|
||||
self.set("website_specifications", [])
|
||||
if self.item_group:
|
||||
for label, desc in frappe.db.get_values(
|
||||
"Item Website Specification", {"parent": self.item_group}, ["label", "description"]
|
||||
):
|
||||
row = self.append("website_specifications")
|
||||
row.label = label
|
||||
row.description = desc
|
||||
|
||||
def get_product_details_section(self, context):
|
||||
"""Get section with tabs or website specifications."""
|
||||
context.show_tabs = self.show_tabbed_section
|
||||
if self.show_tabbed_section and (self.tabs or self.website_specifications):
|
||||
context.tabs = self.get_tabs()
|
||||
else:
|
||||
context.website_specifications = self.website_specifications
|
||||
|
||||
def get_tabs(self):
|
||||
tab_values = {}
|
||||
tab_values["tab_1_title"] = "Product Details"
|
||||
tab_values["tab_1_content"] = frappe.render_template(
|
||||
"templates/generators/item/item_specifications.html",
|
||||
{"website_specifications": self.website_specifications, "show_tabs": self.show_tabbed_section},
|
||||
)
|
||||
|
||||
for row in self.tabs:
|
||||
tab_values[f"tab_{row.idx + 1}_title"] = _(row.label)
|
||||
tab_values[f"tab_{row.idx + 1}_content"] = row.content
|
||||
|
||||
return tab_values
|
||||
|
||||
def get_recommended_items(self, settings):
|
||||
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)"])
|
||||
|
||||
|
||||
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: "Item", save: bool = True) -> Union["WebsiteItem", List[str]]:
|
||||
"Make Website Item from Item. Used via Form UI or patch."
|
||||
|
||||
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",
|
||||
"has_variants",
|
||||
"variant_of",
|
||||
"description",
|
||||
]
|
||||
for field in fields_to_map:
|
||||
website_item.update({field: doc.get(field)})
|
||||
|
||||
# Needed for publishing/mapping via Form UI only
|
||||
if not frappe.flags.in_migrate and (doc.get("image") and not website_item.website_image):
|
||||
website_item.website_image = doc.get("image")
|
||||
|
||||
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]
|
||||
@@ -1,20 +0,0 @@
|
||||
frappe.listview_settings['Website Item'] = {
|
||||
add_fields: ["item_name", "web_item_name", "published", "website_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"];
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
# -*- 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
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
# -*- 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"])
|
||||
@@ -1,117 +0,0 @@
|
||||
# -*- 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()
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Wishlist', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
@@ -1,65 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
# -*- 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},
|
||||
[
|
||||
"website_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("website_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)))
|
||||
@@ -1,147 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
# -*- 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
|
||||
@@ -1,134 +0,0 @@
|
||||
import frappe
|
||||
from frappe.search.full_text_search import FullTextSearch
|
||||
from frappe.utils import strip_html_tags
|
||||
from whoosh.analysis import StemmingAnalyzer
|
||||
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):
|
||||
"""Wrapper for WebsiteSearch"""
|
||||
|
||||
def get_schema(self):
|
||||
return Schema(
|
||||
title=TEXT(stored=True, field_boost=1.5),
|
||||
name=ID(stored=True),
|
||||
path=ID(stored=True),
|
||||
content=TEXT(stored=True, analyzer=StemmingAnalyzer()),
|
||||
keywords=KEYWORD(stored=True, scorable=True, commas=True),
|
||||
)
|
||||
|
||||
def get_id(self):
|
||||
return "name"
|
||||
|
||||
def get_items_to_index(self):
|
||||
"""Get all routes to be indexed, this includes the static pages
|
||||
in www/ and routes from published documents
|
||||
|
||||
Returns:
|
||||
self (object): FullTextSearch Instance
|
||||
"""
|
||||
items = get_all_published_items()
|
||||
documents = [self.get_document_to_index(item) for item in items]
|
||||
return documents
|
||||
|
||||
def get_document_to_index(self, item):
|
||||
try:
|
||||
item = frappe.get_doc("Item", item)
|
||||
title = item.item_name
|
||||
keywords = [item.item_group]
|
||||
|
||||
if item.brand:
|
||||
keywords.append(item.brand)
|
||||
|
||||
if item.website_image_alt:
|
||||
keywords.append(item.website_image_alt)
|
||||
|
||||
if item.has_variants and item.variant_based_on == "Item Attribute":
|
||||
keywords = keywords + [attr.attribute for attr in item.attributes]
|
||||
|
||||
if item.web_long_description:
|
||||
content = strip_html_tags(item.web_long_description)
|
||||
elif item.description:
|
||||
content = strip_html_tags(item.description)
|
||||
|
||||
return frappe._dict(
|
||||
title=title,
|
||||
name=item.name,
|
||||
path=item.route,
|
||||
content=content,
|
||||
keywords=", ".join(keywords),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def search(self, text, scope=None, limit=20):
|
||||
"""Search from the current index
|
||||
|
||||
Args:
|
||||
text (str): String to search for
|
||||
scope (str, optional): Scope to limit the search. Defaults to None.
|
||||
limit (int, optional): Limit number of search results. Defaults to 20.
|
||||
|
||||
Returns:
|
||||
[List(_dict)]: Search results
|
||||
"""
|
||||
ix = self.get_index()
|
||||
|
||||
results = None
|
||||
out = []
|
||||
|
||||
with ix.searcher() as searcher:
|
||||
parser = MultifieldParser(["title", "content", "keywords"], ix.schema)
|
||||
parser.remove_plugin_class(FieldsPlugin)
|
||||
parser.remove_plugin_class(WildcardPlugin)
|
||||
query = parser.parse(text)
|
||||
|
||||
filter_scoped = None
|
||||
if scope:
|
||||
filter_scoped = Prefix(self.id, scope)
|
||||
results = searcher.search(query, limit=limit, filter=filter_scoped)
|
||||
|
||||
for r in results:
|
||||
out.append(self.parse_result(r))
|
||||
|
||||
return out
|
||||
|
||||
def parse_result(self, result):
|
||||
title_highlights = result.highlights("title")
|
||||
content_highlights = result.highlights("content")
|
||||
keyword_highlights = result.highlights("keywords")
|
||||
|
||||
return frappe._dict(
|
||||
title=result["title"],
|
||||
path=result["path"],
|
||||
keywords=result["keywords"],
|
||||
title_highlights=title_highlights,
|
||||
content_highlights=content_highlights,
|
||||
keyword_highlights=keyword_highlights,
|
||||
)
|
||||
|
||||
|
||||
def get_all_published_items():
|
||||
return frappe.get_all(
|
||||
"Website Item", filters={"variant_of": "", "published": 1}, pluck="item_code"
|
||||
)
|
||||
|
||||
|
||||
def update_index_for_path(path):
|
||||
search = ProductSearch(INDEX_NAME)
|
||||
return search.update_index_by_name(path)
|
||||
|
||||
|
||||
def remove_document_from_index(path):
|
||||
search = ProductSearch(INDEX_NAME)
|
||||
return search.remove_document_from_index(path)
|
||||
|
||||
|
||||
def build_index_for_all_routes():
|
||||
search = ProductSearch(INDEX_NAME)
|
||||
return search.build()
|
||||
@@ -1,158 +0,0 @@
|
||||
# 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):
|
||||
from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website
|
||||
|
||||
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 Website Item
|
||||
web_item_meta = frappe.get_meta("Website Item", cached=True)
|
||||
fields = [
|
||||
web_item_meta.get_field(field) for field in filter_fields if web_item_meta.has_field(field)
|
||||
]
|
||||
|
||||
for df in fields:
|
||||
item_filters, item_or_filters = {"published": 1}, []
|
||||
link_doctype_values = self.get_filtered_link_doctype_records(df)
|
||||
|
||||
if df.fieldtype == "Link":
|
||||
if self.item_group:
|
||||
include_child = frappe.db.get_value("Item Group", self.item_group, "include_descendants")
|
||||
if include_child:
|
||||
include_groups = get_child_groups_for_website(self.item_group, include_self=True)
|
||||
include_groups = [x.name for x in include_groups]
|
||||
item_or_filters.extend(
|
||||
[
|
||||
["item_group", "in", include_groups],
|
||||
["Website Item Group", "item_group", "=", self.item_group], # consider website item groups
|
||||
]
|
||||
)
|
||||
else:
|
||||
item_or_filters.extend(
|
||||
[
|
||||
["item_group", "=", self.item_group],
|
||||
["Website Item Group", "item_group", "=", self.item_group], # consider website item groups
|
||||
]
|
||||
)
|
||||
|
||||
# exclude variants if mentioned in settings
|
||||
if frappe.db.get_single_value("E Commerce Settings", "hide_variants"):
|
||||
item_filters["variant_of"] = ["is", "not set"]
|
||||
|
||||
# Get link field values attached to published items
|
||||
item_values = frappe.get_all(
|
||||
"Website 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
|
||||
@@ -1,321 +0,0 @@
|
||||
# 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",
|
||||
"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
|
||||
|
||||
if fields:
|
||||
self.build_fields_filters(fields)
|
||||
if item_group:
|
||||
self.build_item_group_filters(item_group)
|
||||
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)
|
||||
|
||||
# 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_item_group_filters(self, item_group):
|
||||
"Add filters for Item group page and include Website Item Groups."
|
||||
from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website
|
||||
|
||||
item_group_filters = []
|
||||
|
||||
item_group_filters.append(["Website Item", "item_group", "=", item_group])
|
||||
# Consider Website Item Groups
|
||||
item_group_filters.append(["Website Item Group", "item_group", "=", item_group])
|
||||
|
||||
if frappe.db.get_value("Item Group", item_group, "include_descendants"):
|
||||
# include child item group's items as well
|
||||
# eg. Group Node A, will show items of child 1 and child 2 as well
|
||||
# on it's web page
|
||||
include_groups = get_child_groups_for_website(item_group, include_self=True)
|
||||
include_groups = [x.name for x in include_groups]
|
||||
item_group_filters.append(["Website Item", "item_group", "in", include_groups])
|
||||
|
||||
self.or_filters.extend(item_group_filters)
|
||||
|
||||
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 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):
|
||||
from erpnext.templates.pages.wishlist import (
|
||||
get_stock_availability as get_stock_availability_from_template,
|
||||
)
|
||||
|
||||
"""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:
|
||||
item.in_stock = get_stock_availability_from_template(item.item_code, warehouse)
|
||||
|
||||
def get_cart_items(self):
|
||||
customer = get_customer(silent=True)
|
||||
if customer:
|
||||
quotation = frappe.get_all(
|
||||
"Quotation",
|
||||
fields=["name"],
|
||||
filters={
|
||||
"party_name": customer,
|
||||
"contact_email": frappe.session.user,
|
||||
"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 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
|
||||
@@ -1,170 +0,0 @@
|
||||
# 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."
|
||||
|
||||
def setUp(self):
|
||||
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)
|
||||
|
||||
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", 1)
|
||||
|
||||
def tearDown(self):
|
||||
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 - 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)
|
||||
|
||||
def test_item_group_page_with_descendants_included(self):
|
||||
"""
|
||||
Test if 'include_descendants' pulls Items belonging to descendant Item Groups (Level 2 & 3).
|
||||
> _Test Item Group B [Level 1]
|
||||
> _Test Item Group B - 1 [Level 2]
|
||||
> _Test Item Group B - 1 - 1 [Level 3]
|
||||
"""
|
||||
frappe.get_doc(
|
||||
{ # create Level 3 nested child group
|
||||
"doctype": "Item Group",
|
||||
"is_group": 1,
|
||||
"item_group_name": "_Test Item Group B - 1 - 1",
|
||||
"parent_item_group": "_Test Item Group B - 1",
|
||||
}
|
||||
).insert()
|
||||
|
||||
create_regular_web_item( # create an item belonging to level 3 item group
|
||||
"Test Mobile F", item_args={"item_group": "_Test Item Group B - 1 - 1"}
|
||||
)
|
||||
|
||||
frappe.db.set_value("Item Group", "_Test Item Group B - 1 - 1", "show_in_website", 1)
|
||||
|
||||
# enable 'include descendants' in Level 1
|
||||
frappe.db.set_value("Item Group", "_Test Item Group B", "include_descendants", 1)
|
||||
|
||||
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]
|
||||
|
||||
# check if all sub groups' items are pulled
|
||||
self.assertEqual(len(items), 6)
|
||||
self.assertIn("Test Mobile A", item_codes)
|
||||
self.assertIn("Test Mobile C", item_codes)
|
||||
self.assertIn("Test Mobile E", item_codes)
|
||||
self.assertIn("Test Mobile F", item_codes)
|
||||
@@ -1,348 +0,0 @@
|
||||
# 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 test_custom_field_as_filter(self):
|
||||
"Test if custom field functions as filter correctly."
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
|
||||
create_custom_field(
|
||||
"Website Item",
|
||||
dict(
|
||||
owner="Administrator",
|
||||
fieldname="supplier",
|
||||
label="Supplier",
|
||||
fieldtype="Link",
|
||||
options="Supplier",
|
||||
insert_after="on_backorder",
|
||||
),
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Website Item", {"item_code": "Test 11I Laptop"}, "supplier", "_Test Supplier"
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Website Item", {"item_code": "Test 12I Laptop"}, "supplier", "_Test Supplier 1"
|
||||
)
|
||||
|
||||
settings = frappe.get_doc("E Commerce Settings")
|
||||
settings.append("filter_fields", {"fieldname": "supplier"})
|
||||
settings.save()
|
||||
|
||||
filter_engine = ProductFiltersBuilder()
|
||||
field_filters = filter_engine.get_field_filters()
|
||||
custom_filter = field_filters[1]
|
||||
filter_values = custom_filter[1]
|
||||
|
||||
self.assertEqual(custom_filter[0].options, "Supplier")
|
||||
self.assertEqual(len(filter_values), 2)
|
||||
self.assertIn("_Test Supplier", filter_values)
|
||||
|
||||
# test if custom filter works in query
|
||||
field_filters = {"supplier": "_Test Supplier 1"}
|
||||
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), 1)
|
||||
self.assertEqual(items[0].get("item_code"), "Test 12I Laptop")
|
||||
|
||||
|
||||
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)
|
||||
@@ -1,201 +0,0 @@
|
||||
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;
|
||||
|
||||
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 ``;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,205 +0,0 @@
|
||||
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;
|
||||
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 href="/${ item.route || '#' }">
|
||||
<div class="product-title">
|
||||
${ title }
|
||||
</div>
|
||||
</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 ``;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,244 +0,0 @@
|
||||
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=${encodeURI(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);
|
||||
}
|
||||
};
|
||||
@@ -1,548 +0,0 @@
|
||||
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();
|
||||
});
|
||||
|
||||
// bind filter lookup input box
|
||||
$('.filter-lookup-input').on('keydown', frappe.utils.debounce((e) => {
|
||||
const $input = $(e.target);
|
||||
const keyword = ($input.val() || '').toLowerCase();
|
||||
const $filter_options = $input.next('.filter-options');
|
||||
|
||||
$filter_options.find('.filter-lookup-wrapper').show();
|
||||
$filter_options.find('.filter-lookup-wrapper').each((i, el) => {
|
||||
const $el = $(el);
|
||||
const value = $el.data('value').toLowerCase();
|
||||
if (!value.includes(keyword)) {
|
||||
$el.hide();
|
||||
}
|
||||
});
|
||||
}, 300));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
@@ -1,255 +0,0 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils.redis_wrapper import RedisWrapper
|
||||
from redis import ResponseError
|
||||
from redis.commands.search.field import TagField, TextField
|
||||
from redis.commands.search.indexDefinition import IndexDefinition
|
||||
from redis.commands.search.suggestion import Suggestion
|
||||
|
||||
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_redisearch_enabled():
|
||||
"Return True only if redisearch is loaded and enabled."
|
||||
is_redisearch_enabled = frappe.db.get_single_value("E Commerce Settings", "is_redisearch_enabled")
|
||||
return is_search_module_loaded() and is_redisearch_enabled
|
||||
|
||||
|
||||
def is_search_module_loaded():
|
||||
try:
|
||||
cache = frappe.cache()
|
||||
for module in cache.module_list():
|
||||
if module.get(b"name") == b"search":
|
||||
return True
|
||||
except Exception:
|
||||
return False # handling older redis versions
|
||||
|
||||
|
||||
def if_redisearch_enabled(function):
|
||||
"Decorator to check if Redisearch is enabled."
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
if is_redisearch_enabled():
|
||||
func = function(*args, **kwargs)
|
||||
return func
|
||||
return
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def make_key(key):
|
||||
return frappe.cache().make_key(key)
|
||||
|
||||
|
||||
@if_redisearch_enabled
|
||||
def create_website_items_index():
|
||||
"Creates Index Definition."
|
||||
|
||||
redis = frappe.cache()
|
||||
index = redis.ft(WEBSITE_ITEM_INDEX)
|
||||
|
||||
try:
|
||||
index.dropindex() # drop if already exists
|
||||
except ResponseError:
|
||||
# will most likely raise a ResponseError if index does not exist
|
||||
# ignore and create index
|
||||
pass
|
||||
except Exception:
|
||||
raise_redisearch_error()
|
||||
|
||||
idx_def = IndexDefinition([make_key(WEBSITE_ITEM_KEY_PREFIX)])
|
||||
|
||||
# Index fields mentioned in 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 = [to_search_field(f) for f in idx_fields]
|
||||
|
||||
# TODO: sortable?
|
||||
index.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_enabled
|
||||
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 field, value in web_item.items():
|
||||
super(RedisWrapper, cache).hset(make_key(key), field, value)
|
||||
|
||||
insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name)
|
||||
|
||||
|
||||
@if_redisearch_enabled
|
||||
def insert_to_name_ac(web_name, doc_name):
|
||||
ac = frappe.cache().ft()
|
||||
ac.sugadd(WEBSITE_ITEM_NAME_AUTOCOMPLETE, Suggestion(web_name, payload=doc_name))
|
||||
|
||||
|
||||
def create_web_item_map(website_item_doc):
|
||||
fields_to_index = get_fields_indexed()
|
||||
web_item = {}
|
||||
|
||||
for field in fields_to_index:
|
||||
web_item[field] = website_item_doc.get(field) or ""
|
||||
|
||||
return web_item
|
||||
|
||||
|
||||
@if_redisearch_enabled
|
||||
def update_index_for_item(website_item_doc):
|
||||
# Reinsert to Cache
|
||||
insert_item_to_index(website_item_doc)
|
||||
define_autocomplete_dictionary()
|
||||
|
||||
|
||||
@if_redisearch_enabled
|
||||
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:
|
||||
raise_redisearch_error()
|
||||
|
||||
delete_from_ac_dict(website_item_doc)
|
||||
return True
|
||||
|
||||
|
||||
@if_redisearch_enabled
|
||||
def delete_from_ac_dict(website_item_doc):
|
||||
"""Removes this items's name from autocomplete dictionary"""
|
||||
ac = frappe.cache().ft()
|
||||
ac.sugdel(website_item_doc.web_item_name)
|
||||
|
||||
|
||||
@if_redisearch_enabled
|
||||
def define_autocomplete_dictionary():
|
||||
"""
|
||||
Defines/Redefines an autocomplete search dictionary for Website Item Name.
|
||||
Also creats autocomplete dictionary for Published Item Groups.
|
||||
"""
|
||||
|
||||
cache = frappe.cache()
|
||||
|
||||
# Delete both autocomplete dicts
|
||||
try:
|
||||
cache.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE))
|
||||
cache.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE))
|
||||
except Exception:
|
||||
raise_redisearch_error()
|
||||
|
||||
create_items_autocomplete_dict()
|
||||
create_item_groups_autocomplete_dict()
|
||||
|
||||
|
||||
@if_redisearch_enabled
|
||||
def create_items_autocomplete_dict():
|
||||
"Add items as suggestions in Autocompleter."
|
||||
|
||||
ac = frappe.cache().ft()
|
||||
items = frappe.get_all(
|
||||
"Website Item", fields=["web_item_name", "item_group"], filters={"published": 1}
|
||||
)
|
||||
for item in items:
|
||||
ac.sugadd(WEBSITE_ITEM_NAME_AUTOCOMPLETE, Suggestion(item.web_item_name))
|
||||
|
||||
|
||||
@if_redisearch_enabled
|
||||
def create_item_groups_autocomplete_dict():
|
||||
"Add item groups with weightage as suggestions in Autocompleter."
|
||||
|
||||
published_item_groups = frappe.get_all(
|
||||
"Item Group", fields=["name", "route", "weightage"], filters={"show_in_website": 1}
|
||||
)
|
||||
if not published_item_groups:
|
||||
return
|
||||
|
||||
ac = frappe.cache().ft()
|
||||
|
||||
for item_group in published_item_groups:
|
||||
payload = json.dumps({"name": item_group.name, "route": item_group.route})
|
||||
ac.sugadd(
|
||||
WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE,
|
||||
Suggestion(
|
||||
string=item_group.name,
|
||||
score=frappe.utils.flt(item_group.weightage) or 1.0,
|
||||
payload=payload, # additional info that can be retrieved later
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@if_redisearch_enabled
|
||||
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 field, value in web_item.items():
|
||||
super(RedisWrapper, cache).hset(key, field, value)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def raise_redisearch_error():
|
||||
"Create an Error Log and raise error."
|
||||
log = frappe.log_error("Redisearch Error")
|
||||
log_link = frappe.utils.get_link_to_form("Error Log", log.name)
|
||||
|
||||
frappe.throw(
|
||||
msg=_("Something went wrong. Check {0}").format(log_link), title=_("Redisearch Error")
|
||||
)
|
||||
@@ -1,721 +0,0 @@
|
||||
# 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
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
from frappe.contacts.doctype.contact.contact import get_contact_name
|
||||
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.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||
get_shopping_cart_settings,
|
||||
)
|
||||
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("E Commerce Settings", "enabled")):
|
||||
if not quotation:
|
||||
quotation = _get_cart_quotation()
|
||||
cart_count = cstr(cint(quotation.get("total_qty")))
|
||||
|
||||
if hasattr(frappe.local, "cookie_manager"):
|
||||
frappe.local.cookie_manager.set_cookie("cart_count", cart_count)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_cart_quotation(doc=None):
|
||||
party = get_party()
|
||||
|
||||
if not doc:
|
||||
quotation = _get_cart_quotation(party)
|
||||
doc = quotation
|
||||
set_cart_count(quotation)
|
||||
|
||||
addresses = get_address_docs(party=party)
|
||||
|
||||
if not doc.customer_address and addresses:
|
||||
update_cart_address("billing", addresses[0].name)
|
||||
|
||||
return {
|
||||
"doc": decorate_quotation_doc(doc),
|
||||
"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("E Commerce Settings"),
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_shipping_addresses(party=None):
|
||||
if not party:
|
||||
party = get_party()
|
||||
addresses = get_address_docs(party=party)
|
||||
return [
|
||||
{"name": address.name, "title": address.address_title, "display": address.display}
|
||||
for address in addresses
|
||||
if address.address_type == "Shipping"
|
||||
]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_billing_addresses(party=None):
|
||||
if not party:
|
||||
party = get_party()
|
||||
addresses = get_address_docs(party=party)
|
||||
return [
|
||||
{"name": address.name, "title": address.address_title, "display": address.display}
|
||||
for address in addresses
|
||||
if address.address_type == "Billing"
|
||||
]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def place_order():
|
||||
quotation = _get_cart_quotation()
|
||||
cart_settings = frappe.db.get_value(
|
||||
"E Commerce Settings", None, ["company", "allow_items_not_in_stock"], as_dict=1
|
||||
)
|
||||
quotation.company = cart_settings.company
|
||||
|
||||
quotation.flags.ignore_permissions = True
|
||||
quotation.submit()
|
||||
|
||||
if quotation.quotation_to == "Lead" and quotation.party_name:
|
||||
# company used to create customer accounts
|
||||
frappe.defaults.set_user_default("company", quotation.company)
|
||||
|
||||
if not (quotation.shipping_address_name or quotation.customer_address):
|
||||
frappe.throw(_("Set Shipping Address or Billing Address"))
|
||||
|
||||
from erpnext.selling.doctype.quotation.quotation import _make_sales_order
|
||||
|
||||
sales_order = frappe.get_doc(_make_sales_order(quotation.name, ignore_permissions=True))
|
||||
sales_order.payment_schedule = []
|
||||
|
||||
if not cint(cart_settings.allow_items_not_in_stock):
|
||||
for item in sales_order.get("items"):
|
||||
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_web_item_qty_in_stock(item.item_code, "website_warehouse")
|
||||
if not cint(item_stock.in_stock):
|
||||
throw(_("{0} Not in Stock").format(item.item_code))
|
||||
if item.qty > item_stock.stock_qty:
|
||||
throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty, item.item_code))
|
||||
|
||||
sales_order.flags.ignore_permissions = True
|
||||
sales_order.insert()
|
||||
sales_order.submit()
|
||||
|
||||
if hasattr(frappe.local, "cookie_manager"):
|
||||
frappe.local.cookie_manager.delete_cookie("cart_count")
|
||||
|
||||
return sales_order.name
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def request_for_quotation():
|
||||
quotation = _get_cart_quotation()
|
||||
quotation.flags.ignore_permissions = True
|
||||
|
||||
if get_shopping_cart_settings().save_quotations_as_draft:
|
||||
quotation.save()
|
||||
else:
|
||||
quotation.submit()
|
||||
return quotation.name
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_cart(item_code, qty, additional_notes=None, with_items=False):
|
||||
quotation = _get_cart_quotation()
|
||||
|
||||
empty_card = False
|
||||
qty = flt(qty)
|
||||
if qty == 0:
|
||||
quotation_items = quotation.get("items", {"item_code": ["!=", item_code]})
|
||||
if quotation_items:
|
||||
quotation.set("items", quotation_items)
|
||||
else:
|
||||
empty_card = True
|
||||
|
||||
else:
|
||||
warehouse = frappe.get_cached_value(
|
||||
"Website Item", {"item_code": item_code}, "website_warehouse"
|
||||
)
|
||||
|
||||
quotation_items = quotation.get("items", {"item_code": item_code})
|
||||
if not quotation_items:
|
||||
quotation.append(
|
||||
"items",
|
||||
{
|
||||
"doctype": "Quotation Item",
|
||||
"item_code": item_code,
|
||||
"qty": qty,
|
||||
"additional_notes": additional_notes,
|
||||
"warehouse": warehouse,
|
||||
},
|
||||
)
|
||||
else:
|
||||
quotation_items[0].qty = qty
|
||||
quotation_items[0].additional_notes = additional_notes
|
||||
quotation_items[0].warehouse = warehouse
|
||||
|
||||
apply_cart_settings(quotation=quotation)
|
||||
|
||||
quotation.flags.ignore_permissions = True
|
||||
quotation.payment_schedule = []
|
||||
if not empty_card:
|
||||
quotation.save()
|
||||
else:
|
||||
quotation.delete()
|
||||
quotation = None
|
||||
|
||||
set_cart_count(quotation)
|
||||
|
||||
if cint(with_items):
|
||||
context = get_cart_quotation(quotation)
|
||||
return {
|
||||
"items": frappe.render_template("templates/includes/cart/cart_items.html", context),
|
||||
"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}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_shopping_cart_menu(context=None):
|
||||
if not context:
|
||||
context = get_cart_quotation()
|
||||
|
||||
return frappe.render_template("templates/includes/cart/cart_dropdown.html", context)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_new_address(doc):
|
||||
doc = frappe.parse_json(doc)
|
||||
doc.update({"doctype": "Address"})
|
||||
address = frappe.get_doc(doc)
|
||||
address.save(ignore_permissions=True)
|
||||
|
||||
return address
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def create_lead_for_item_inquiry(lead, subject, message):
|
||||
lead = frappe.parse_json(lead)
|
||||
lead_doc = frappe.new_doc("Lead")
|
||||
for fieldname in ("lead_name", "company_name", "email_id", "phone"):
|
||||
lead_doc.set(fieldname, lead.get(fieldname))
|
||||
|
||||
lead_doc.set("lead_owner", "")
|
||||
|
||||
if not frappe.db.exists("Lead Source", "Product Inquiry"):
|
||||
frappe.get_doc({"doctype": "Lead Source", "source_name": "Product Inquiry"}).insert(
|
||||
ignore_permissions=True
|
||||
)
|
||||
|
||||
lead_doc.set("source", "Product Inquiry")
|
||||
|
||||
try:
|
||||
lead_doc.save(ignore_permissions=True)
|
||||
except frappe.exceptions.DuplicateEntryError:
|
||||
frappe.clear_messages()
|
||||
lead_doc = frappe.get_doc("Lead", {"email_id": lead["email_id"]})
|
||||
|
||||
lead_doc.add_comment(
|
||||
"Comment",
|
||||
text="""
|
||||
<div>
|
||||
<h5>{subject}</h5>
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
""".format(
|
||||
subject=subject, message=message
|
||||
),
|
||||
)
|
||||
|
||||
return lead_doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_terms_and_conditions(terms_name):
|
||||
return frappe.db.get_value("Terms and Conditions", terms_name, "terms")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_cart_address(address_type, address_name):
|
||||
quotation = _get_cart_quotation()
|
||||
address_doc = frappe.get_doc("Address", address_name).as_dict()
|
||||
address_display = get_address_display(address_doc)
|
||||
|
||||
if address_type.lower() == "billing":
|
||||
quotation.customer_address = address_name
|
||||
quotation.address_display = address_display
|
||||
quotation.shipping_address_name = quotation.shipping_address_name or address_name
|
||||
address_doc = next((doc for doc in get_billing_addresses() if doc["name"] == address_name), None)
|
||||
elif address_type.lower() == "shipping":
|
||||
quotation.shipping_address_name = address_name
|
||||
quotation.shipping_address = address_display
|
||||
quotation.customer_address = quotation.customer_address or address_name
|
||||
address_doc = next(
|
||||
(doc for doc in get_shipping_addresses() if doc["name"] == address_name), None
|
||||
)
|
||||
apply_cart_settings(quotation=quotation)
|
||||
|
||||
quotation.flags.ignore_permissions = True
|
||||
quotation.save()
|
||||
|
||||
context = get_cart_quotation(quotation)
|
||||
context["address"] = address_doc
|
||||
|
||||
return {
|
||||
"taxes": frappe.render_template("templates/includes/order/order_taxes.html", context),
|
||||
"address": frappe.render_template("templates/includes/cart/address_card.html", context),
|
||||
}
|
||||
|
||||
|
||||
def guess_territory():
|
||||
territory = None
|
||||
geoip_country = frappe.session.get("session_country")
|
||||
if geoip_country:
|
||||
territory = frappe.db.get_value("Territory", geoip_country)
|
||||
|
||||
return (
|
||||
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", []):
|
||||
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))
|
||||
website_warehouse = frappe.get_cached_value(
|
||||
"Website Item", {"item_code": item_code}, "website_warehouse"
|
||||
)
|
||||
d.warehouse = website_warehouse
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def _get_cart_quotation(party=None):
|
||||
"""Return the open Quotation of type "Shopping Cart" or make a new one"""
|
||||
if not party:
|
||||
party = get_party()
|
||||
|
||||
quotation = frappe.get_all(
|
||||
"Quotation",
|
||||
fields=["name"],
|
||||
filters={
|
||||
"party_name": party.name,
|
||||
"contact_email": frappe.session.user,
|
||||
"order_type": "Shopping Cart",
|
||||
"docstatus": 0,
|
||||
},
|
||||
order_by="modified desc",
|
||||
limit_page_length=1,
|
||||
)
|
||||
|
||||
if quotation:
|
||||
qdoc = frappe.get_doc("Quotation", quotation[0].name)
|
||||
else:
|
||||
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-",
|
||||
"quotation_to": party.doctype,
|
||||
"company": company,
|
||||
"order_type": "Shopping Cart",
|
||||
"status": "Draft",
|
||||
"docstatus": 0,
|
||||
"__islocal": 1,
|
||||
"party_name": party.name,
|
||||
}
|
||||
)
|
||||
|
||||
qdoc.contact_person = frappe.db.get_value("Contact", {"email_id": frappe.session.user})
|
||||
qdoc.contact_email = frappe.session.user
|
||||
|
||||
qdoc.flags.ignore_permissions = True
|
||||
qdoc.run_method("set_missing_values")
|
||||
apply_cart_settings(party, qdoc)
|
||||
|
||||
return qdoc
|
||||
|
||||
|
||||
def update_party(fullname, company_name=None, mobile_no=None, phone=None):
|
||||
party = get_party()
|
||||
|
||||
party.customer_name = company_name or fullname
|
||||
party.customer_type = "Company" if company_name else "Individual"
|
||||
|
||||
contact_name = frappe.db.get_value("Contact", {"email_id": frappe.session.user})
|
||||
contact = frappe.get_doc("Contact", contact_name)
|
||||
contact.first_name = fullname
|
||||
contact.last_name = None
|
||||
contact.customer_name = party.customer_name
|
||||
contact.mobile_no = mobile_no
|
||||
contact.phone = phone
|
||||
contact.flags.ignore_permissions = True
|
||||
contact.save()
|
||||
|
||||
party_doc = frappe.get_doc(party.as_dict())
|
||||
party_doc.flags.ignore_permissions = True
|
||||
party_doc.save()
|
||||
|
||||
qdoc = _get_cart_quotation(party)
|
||||
if not qdoc.get("__islocal"):
|
||||
qdoc.customer_name = company_name or fullname
|
||||
qdoc.run_method("set_missing_lead_customer_details")
|
||||
qdoc.flags.ignore_permissions = True
|
||||
qdoc.save()
|
||||
|
||||
|
||||
def apply_cart_settings(party=None, quotation=None):
|
||||
if not party:
|
||||
party = get_party()
|
||||
if not quotation:
|
||||
quotation = _get_cart_quotation(party)
|
||||
|
||||
cart_settings = frappe.get_doc("E Commerce Settings")
|
||||
|
||||
set_price_list_and_rate(quotation, cart_settings)
|
||||
|
||||
quotation.run_method("calculate_taxes_and_totals")
|
||||
|
||||
set_taxes(quotation, cart_settings)
|
||||
|
||||
_apply_shipping_rule(party, quotation, cart_settings)
|
||||
|
||||
|
||||
def set_price_list_and_rate(quotation, cart_settings):
|
||||
"""set price list based on billing territory"""
|
||||
|
||||
_set_price_list(cart_settings, quotation)
|
||||
|
||||
# reset values
|
||||
quotation.price_list_currency = (
|
||||
quotation.currency
|
||||
) = quotation.plc_conversion_rate = quotation.conversion_rate = None
|
||||
for item in quotation.get("items"):
|
||||
item.price_list_rate = item.discount_percentage = item.rate = item.amount = None
|
||||
|
||||
# refetch values
|
||||
quotation.run_method("set_price_list_and_item_details")
|
||||
|
||||
if hasattr(frappe.local, "cookie_manager"):
|
||||
# set it in cookies for using in product page
|
||||
frappe.local.cookie_manager.set_cookie("selling_price_list", quotation.selling_price_list)
|
||||
|
||||
|
||||
def _set_price_list(cart_settings, quotation=None):
|
||||
"""Set price list based on customer or shopping cart default"""
|
||||
from erpnext.accounts.party import get_default_price_list
|
||||
|
||||
party_name = quotation.get("party_name") if quotation else get_party().get("name")
|
||||
selling_price_list = None
|
||||
|
||||
# check if default customer price list exists
|
||||
if party_name and frappe.db.exists("Customer", party_name):
|
||||
selling_price_list = get_default_price_list(frappe.get_doc("Customer", party_name))
|
||||
|
||||
# check default price list in shopping cart
|
||||
if not selling_price_list:
|
||||
selling_price_list = cart_settings.price_list
|
||||
|
||||
if quotation:
|
||||
quotation.selling_price_list = selling_price_list
|
||||
|
||||
return selling_price_list
|
||||
|
||||
|
||||
def set_taxes(quotation, cart_settings):
|
||||
"""set taxes based on billing territory"""
|
||||
from erpnext.accounts.party import set_taxes
|
||||
|
||||
customer_group = frappe.db.get_value("Customer", quotation.party_name, "customer_group")
|
||||
|
||||
quotation.taxes_and_charges = set_taxes(
|
||||
quotation.party_name,
|
||||
"Customer",
|
||||
quotation.transaction_date,
|
||||
quotation.company,
|
||||
customer_group=customer_group,
|
||||
supplier_group=None,
|
||||
tax_category=quotation.tax_category,
|
||||
billing_address=quotation.customer_address,
|
||||
shipping_address=quotation.shipping_address_name,
|
||||
use_for_shopping_cart=1,
|
||||
)
|
||||
#
|
||||
# # clear table
|
||||
quotation.set("taxes", [])
|
||||
#
|
||||
# # append taxes
|
||||
quotation.append_taxes_from_master()
|
||||
|
||||
|
||||
def get_party(user=None):
|
||||
if not user:
|
||||
user = frappe.session.user
|
||||
|
||||
contact_name = get_contact_name(user)
|
||||
party = None
|
||||
|
||||
if contact_name:
|
||||
contact = frappe.get_doc("Contact", contact_name)
|
||||
if contact.links:
|
||||
party_doctype = contact.links[0].link_doctype
|
||||
party = contact.links[0].link_name
|
||||
|
||||
cart_settings = frappe.get_doc("E Commerce Settings")
|
||||
|
||||
debtors_account = ""
|
||||
|
||||
if cart_settings.enable_checkout:
|
||||
debtors_account = get_debtors_account(cart_settings)
|
||||
|
||||
if party:
|
||||
return frappe.get_doc(party_doctype, party)
|
||||
|
||||
else:
|
||||
if not cart_settings.enabled:
|
||||
frappe.local.flags.redirect_location = "/contact"
|
||||
raise frappe.Redirect
|
||||
customer = frappe.new_doc("Customer")
|
||||
fullname = get_fullname(user)
|
||||
customer.update(
|
||||
{
|
||||
"customer_name": fullname,
|
||||
"customer_type": "Individual",
|
||||
"customer_group": get_shopping_cart_settings().default_customer_group,
|
||||
"territory": get_root_of("Territory"),
|
||||
}
|
||||
)
|
||||
|
||||
customer.append("portal_users", {"user": user})
|
||||
|
||||
if debtors_account:
|
||||
customer.update({"accounts": [{"company": cart_settings.company, "account": debtors_account}]})
|
||||
|
||||
customer.flags.ignore_mandatory = True
|
||||
customer.insert(ignore_permissions=True)
|
||||
|
||||
contact = frappe.new_doc("Contact")
|
||||
contact.update({"first_name": fullname, "email_ids": [{"email_id": user, "is_primary": 1}]})
|
||||
contact.append("links", dict(link_doctype="Customer", link_name=customer.name))
|
||||
contact.flags.ignore_mandatory = True
|
||||
contact.insert(ignore_permissions=True)
|
||||
|
||||
return customer
|
||||
|
||||
|
||||
def get_debtors_account(cart_settings):
|
||||
if not cart_settings.payment_gateway_account:
|
||||
frappe.throw(_("Payment Gateway Account not set"), _("Mandatory"))
|
||||
|
||||
payment_gateway_account_currency = frappe.get_doc(
|
||||
"Payment Gateway Account", cart_settings.payment_gateway_account
|
||||
).currency
|
||||
|
||||
account_name = _("Debtors ({0})").format(payment_gateway_account_currency)
|
||||
|
||||
debtors_account_name = get_account_name(
|
||||
"Receivable",
|
||||
"Asset",
|
||||
is_group=0,
|
||||
account_currency=payment_gateway_account_currency,
|
||||
company=cart_settings.company,
|
||||
)
|
||||
|
||||
if not debtors_account_name:
|
||||
debtors_account = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_type": "Receivable",
|
||||
"root_type": "Asset",
|
||||
"is_group": 0,
|
||||
"parent_account": get_account_name(
|
||||
root_type="Asset", is_group=1, company=cart_settings.company
|
||||
),
|
||||
"account_name": account_name,
|
||||
"currency": payment_gateway_account_currency,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
return debtors_account.name
|
||||
|
||||
else:
|
||||
return debtors_account_name
|
||||
|
||||
|
||||
def get_address_docs(
|
||||
doctype=None, txt=None, filters=None, limit_start=0, limit_page_length=20, party=None
|
||||
):
|
||||
if not party:
|
||||
party = get_party()
|
||||
|
||||
if not party:
|
||||
return []
|
||||
|
||||
address_names = frappe.db.get_all(
|
||||
"Dynamic Link",
|
||||
fields=("parent"),
|
||||
filters=dict(parenttype="Address", link_doctype=party.doctype, link_name=party.name),
|
||||
)
|
||||
|
||||
out = []
|
||||
|
||||
for a in address_names:
|
||||
address = frappe.get_doc("Address", a.parent)
|
||||
address.display = get_address_display(address.as_dict())
|
||||
out.append(address)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def apply_shipping_rule(shipping_rule):
|
||||
quotation = _get_cart_quotation()
|
||||
|
||||
quotation.shipping_rule = shipping_rule
|
||||
|
||||
apply_cart_settings(quotation=quotation)
|
||||
|
||||
quotation.flags.ignore_permissions = True
|
||||
quotation.save()
|
||||
|
||||
return get_cart_quotation(quotation)
|
||||
|
||||
|
||||
def _apply_shipping_rule(party=None, quotation=None, cart_settings=None):
|
||||
if not quotation.shipping_rule:
|
||||
shipping_rules = get_shipping_rules(quotation, cart_settings)
|
||||
|
||||
if not shipping_rules:
|
||||
return
|
||||
|
||||
elif quotation.shipping_rule not in shipping_rules:
|
||||
quotation.shipping_rule = shipping_rules[0]
|
||||
|
||||
if quotation.shipping_rule:
|
||||
quotation.run_method("apply_shipping_rule")
|
||||
quotation.run_method("calculate_taxes_and_totals")
|
||||
|
||||
|
||||
def get_applicable_shipping_rules(party=None, quotation=None):
|
||||
shipping_rules = get_shipping_rules(quotation)
|
||||
|
||||
if shipping_rules:
|
||||
# we need this in sorted order as per the position of the rule in the settings page
|
||||
return [[rule, rule] for rule in shipping_rules]
|
||||
|
||||
|
||||
def get_shipping_rules(quotation=None, cart_settings=None):
|
||||
if not quotation:
|
||||
quotation = _get_cart_quotation()
|
||||
|
||||
shipping_rules = []
|
||||
if quotation.shipping_address_name:
|
||||
country = frappe.db.get_value("Address", quotation.shipping_address_name, "country")
|
||||
if 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
|
||||
|
||||
|
||||
def get_address_territory(address_name):
|
||||
"""Tries to match city, state and country of address to existing territory"""
|
||||
territory = None
|
||||
|
||||
if address_name:
|
||||
address_fields = frappe.db.get_value("Address", address_name, ["city", "state", "country"])
|
||||
for value in address_fields:
|
||||
territory = frappe.db.get_value("Territory", value)
|
||||
if territory:
|
||||
break
|
||||
|
||||
return territory
|
||||
|
||||
|
||||
def show_terms(doc):
|
||||
return doc.tc_name
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def apply_coupon_code(applied_code, applied_referral_sales_partner):
|
||||
quotation = True
|
||||
|
||||
if not applied_code:
|
||||
frappe.throw(_("Please enter a coupon code"))
|
||||
|
||||
coupon_list = frappe.get_all("Coupon Code", filters={"coupon_code": applied_code})
|
||||
if not coupon_list:
|
||||
frappe.throw(_("Please enter a valid coupon code"))
|
||||
|
||||
coupon_name = coupon_list[0].name
|
||||
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
|
||||
|
||||
validate_coupon_code(coupon_name)
|
||||
quotation = _get_cart_quotation()
|
||||
quotation.coupon_code = coupon_name
|
||||
quotation.flags.ignore_permissions = True
|
||||
quotation.save()
|
||||
|
||||
if applied_referral_sales_partner:
|
||||
sales_partner_list = frappe.get_all(
|
||||
"Sales Partner", filters={"referral_code": applied_referral_sales_partner}
|
||||
)
|
||||
if sales_partner_list:
|
||||
sales_partner_name = sales_partner_list[0].name
|
||||
quotation.referral_sales_partner = sales_partner_name
|
||||
quotation.flags.ignore_permissions = True
|
||||
quotation.save()
|
||||
|
||||
return quotation
|
||||
@@ -1,99 +0,0 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||
get_shopping_cart_settings,
|
||||
show_quantity_in_website,
|
||||
)
|
||||
from erpnext.e_commerce.shopping_cart.cart import _get_cart_quotation, _set_price_list
|
||||
from erpnext.utilities.product import (
|
||||
get_non_stock_item_status,
|
||||
get_price,
|
||||
get_web_item_qty_in_stock,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_product_info_for_website(item_code, skip_quotation_creation=False):
|
||||
"""get product price / stock info for website"""
|
||||
|
||||
cart_settings = get_shopping_cart_settings()
|
||||
if not cart_settings.enabled:
|
||||
# return settings even if cart is disabled
|
||||
return frappe._dict({"product_info": {}, "cart_settings": cart_settings})
|
||||
|
||||
cart_quotation = frappe._dict()
|
||||
if not skip_quotation_creation:
|
||||
cart_quotation = _get_cart_quotation()
|
||||
|
||||
selling_price_list = (
|
||||
cart_quotation.get("selling_price_list")
|
||||
if cart_quotation
|
||||
else _set_price_list(cart_settings, None)
|
||||
)
|
||||
|
||||
price = {}
|
||||
if cart_settings.show_price:
|
||||
is_guest = frappe.session.user == "Guest"
|
||||
# Show Price if logged in.
|
||||
# If not logged in, check if price is hidden for guest.
|
||||
if not is_guest or not cart_settings.hide_price_for_guest:
|
||||
price = get_price(
|
||||
item_code, selling_price_list, cart_settings.default_customer_group, cart_settings.company
|
||||
)
|
||||
|
||||
stock_status = None
|
||||
|
||||
if cart_settings.show_stock_availability:
|
||||
on_backorder = frappe.get_cached_value("Website Item", {"item_code": item_code}, "on_backorder")
|
||||
if on_backorder:
|
||||
stock_status = frappe._dict({"on_backorder": True})
|
||||
else:
|
||||
stock_status = get_web_item_qty_in_stock(item_code, "website_warehouse")
|
||||
|
||||
product_info = {
|
||||
"price": price,
|
||||
"qty": 0,
|
||||
"uom": frappe.db.get_value("Item", item_code, "stock_uom"),
|
||||
"sales_uom": frappe.db.get_value("Item", item_code, "sales_uom"),
|
||||
}
|
||||
|
||||
if stock_status:
|
||||
if stock_status.on_backorder:
|
||||
product_info["on_backorder"] = True
|
||||
else:
|
||||
product_info["stock_qty"] = stock_status.stock_qty
|
||||
product_info["in_stock"] = (
|
||||
stock_status.in_stock
|
||||
if stock_status.is_stock_item
|
||||
else get_non_stock_item_status(item_code, "website_warehouse")
|
||||
)
|
||||
product_info["show_stock_qty"] = show_quantity_in_website()
|
||||
|
||||
if product_info["price"]:
|
||||
if frappe.session.user != "Guest":
|
||||
item = cart_quotation.get({"item_code": item_code}) if cart_quotation else None
|
||||
if item:
|
||||
product_info["qty"] = item[0].qty
|
||||
|
||||
return frappe._dict({"product_info": product_info, "cart_settings": cart_settings})
|
||||
|
||||
|
||||
def set_product_info_for_website(item):
|
||||
"""set product price uom for website"""
|
||||
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get(
|
||||
"product_info"
|
||||
)
|
||||
|
||||
if product_info:
|
||||
item.update(product_info)
|
||||
item["stock_uom"] = product_info.get("uom")
|
||||
item["sales_uom"] = product_info.get("sales_uom")
|
||||
if product_info.get("price"):
|
||||
item["price_stock_uom"] = product_info.get("price").get("formatted_price")
|
||||
item["price_sales_uom"] = product_info.get("price").get("formatted_price_sales_uom")
|
||||
else:
|
||||
item["price_stock_uom"] = ""
|
||||
item["price_sales_uom"] = ""
|
||||
@@ -1,398 +0,0 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import change_settings
|
||||
from frappe.utils import add_months, cint, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule
|
||||
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,
|
||||
request_for_quotation,
|
||||
update_cart,
|
||||
)
|
||||
|
||||
|
||||
class TestShoppingCart(unittest.TestCase):
|
||||
"""
|
||||
Note:
|
||||
Shopping Cart == Quotation
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
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()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
frappe.db.sql("delete from `tabTax Rule`")
|
||||
|
||||
def test_get_cart_new_user(self):
|
||||
self.login_as_customer(
|
||||
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
|
||||
)
|
||||
create_address_and_contact(
|
||||
address_title="_Test Address for Customer 2",
|
||||
first_name="_Test Contact for Customer 2",
|
||||
email="test_contact_two_customer@example.com",
|
||||
customer="_Test Customer 2",
|
||||
)
|
||||
# test if lead is created and quotation with new lead is fetched
|
||||
customer = frappe.get_doc("Customer", "_Test Customer 2")
|
||||
quotation = _get_cart_quotation(party=customer)
|
||||
self.assertEqual(quotation.quotation_to, "Customer")
|
||||
self.assertEqual(
|
||||
quotation.contact_person,
|
||||
frappe.db.get_value("Contact", dict(email_id="test_contact_two_customer@example.com")),
|
||||
)
|
||||
self.assertEqual(quotation.contact_email, frappe.session.user)
|
||||
|
||||
return quotation
|
||||
|
||||
def test_get_cart_customer(self, customer="_Test Customer 2"):
|
||||
def validate_quotation(customer_name):
|
||||
# test if quotation with customer is fetched
|
||||
party = frappe.get_doc("Customer", customer_name)
|
||||
quotation = _get_cart_quotation(party=party)
|
||||
self.assertEqual(quotation.quotation_to, "Customer")
|
||||
self.assertEqual(quotation.party_name, customer_name)
|
||||
self.assertEqual(quotation.contact_email, frappe.session.user)
|
||||
return quotation
|
||||
|
||||
quotation = validate_quotation(customer)
|
||||
return quotation
|
||||
|
||||
def test_add_to_cart(self):
|
||||
self.login_as_customer(
|
||||
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
|
||||
)
|
||||
create_address_and_contact(
|
||||
address_title="_Test Address for Customer 2",
|
||||
first_name="_Test Contact for Customer 2",
|
||||
email="test_contact_two_customer@example.com",
|
||||
customer="_Test Customer 2",
|
||||
)
|
||||
# clear existing quotations
|
||||
self.clear_existing_quotations()
|
||||
|
||||
# add first item
|
||||
update_cart("_Test Item", 1)
|
||||
|
||||
quotation = self.test_get_cart_customer("_Test Customer 2")
|
||||
|
||||
self.assertEqual(quotation.get("items")[0].item_code, "_Test Item")
|
||||
self.assertEqual(quotation.get("items")[0].qty, 1)
|
||||
self.assertEqual(quotation.get("items")[0].amount, 10)
|
||||
|
||||
# add second item
|
||||
update_cart("_Test Item 2", 1)
|
||||
quotation = self.test_get_cart_customer("_Test Customer 2")
|
||||
self.assertEqual(quotation.get("items")[1].item_code, "_Test Item 2")
|
||||
self.assertEqual(quotation.get("items")[1].qty, 1)
|
||||
self.assertEqual(quotation.get("items")[1].amount, 20)
|
||||
|
||||
self.assertEqual(len(quotation.get("items")), 2)
|
||||
|
||||
def test_update_cart(self):
|
||||
# first, add to cart
|
||||
self.test_add_to_cart()
|
||||
|
||||
# update first item
|
||||
update_cart("_Test Item", 5)
|
||||
quotation = self.test_get_cart_customer("_Test Customer 2")
|
||||
self.assertEqual(quotation.get("items")[0].item_code, "_Test Item")
|
||||
self.assertEqual(quotation.get("items")[0].qty, 5)
|
||||
self.assertEqual(quotation.get("items")[0].amount, 50)
|
||||
self.assertEqual(quotation.net_total, 70)
|
||||
self.assertEqual(len(quotation.get("items")), 2)
|
||||
|
||||
def test_remove_from_cart(self):
|
||||
# first, add to cart
|
||||
self.test_add_to_cart()
|
||||
|
||||
# remove first item
|
||||
update_cart("_Test Item", 0)
|
||||
quotation = self.test_get_cart_customer("_Test Customer 2")
|
||||
|
||||
self.assertEqual(quotation.get("items")[0].item_code, "_Test Item 2")
|
||||
self.assertEqual(quotation.get("items")[0].qty, 1)
|
||||
self.assertEqual(quotation.get("items")[0].amount, 20)
|
||||
self.assertEqual(quotation.net_total, 20)
|
||||
self.assertEqual(len(quotation.get("items")), 1)
|
||||
|
||||
@unittest.skip("Flaky in CI")
|
||||
def test_tax_rule(self):
|
||||
self.create_tax_rule()
|
||||
|
||||
self.login_as_customer(
|
||||
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
|
||||
)
|
||||
create_address_and_contact(
|
||||
address_title="_Test Address for Customer 2",
|
||||
first_name="_Test Contact for Customer 2",
|
||||
email="test_contact_two_customer@example.com",
|
||||
customer="_Test Customer 2",
|
||||
)
|
||||
|
||||
quotation = self.create_quotation()
|
||||
|
||||
from erpnext.accounts.party import set_taxes
|
||||
|
||||
tax_rule_master = set_taxes(
|
||||
quotation.party_name,
|
||||
"Customer",
|
||||
None,
|
||||
quotation.company,
|
||||
customer_group=None,
|
||||
supplier_group=None,
|
||||
tax_category=quotation.tax_category,
|
||||
billing_address=quotation.customer_address,
|
||||
shipping_address=quotation.shipping_address_name,
|
||||
use_for_shopping_cart=1,
|
||||
)
|
||||
|
||||
self.assertEqual(quotation.taxes_and_charges, tax_rule_master)
|
||||
self.assertEqual(quotation.total_taxes_and_charges, 1000.0)
|
||||
|
||||
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)
|
||||
|
||||
@change_settings("E Commerce Settings", {"save_quotations_as_draft": 1})
|
||||
def test_cart_without_checkout_and_draft_quotation(self):
|
||||
"Test impact of 'save_quotations_as_draft' checkbox."
|
||||
frappe.local.shopping_cart_settings = None
|
||||
|
||||
# add item to cart
|
||||
update_cart("_Test Item", 1)
|
||||
quote_name = request_for_quotation() # Request for Quote
|
||||
quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus"))
|
||||
|
||||
self.assertEqual(quote_doctstatus, 0)
|
||||
|
||||
frappe.db.set_single_value("E Commerce Settings", "save_quotations_as_draft", 0)
|
||||
frappe.local.shopping_cart_settings = None
|
||||
update_cart("_Test Item", 1)
|
||||
quote_name = request_for_quotation() # Request for Quote
|
||||
quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus"))
|
||||
|
||||
self.assertEqual(quote_doctstatus, 1)
|
||||
|
||||
def create_tax_rule(self):
|
||||
tax_rule = frappe.get_test_records("Tax Rule")[0]
|
||||
try:
|
||||
frappe.get_doc(tax_rule).insert(ignore_if_duplicate=True)
|
||||
except (frappe.DuplicateEntryError, ConflictingTaxRule):
|
||||
pass
|
||||
|
||||
def create_quotation(self):
|
||||
quotation = frappe.new_doc("Quotation")
|
||||
|
||||
values = {
|
||||
"doctype": "Quotation",
|
||||
"quotation_to": "Customer",
|
||||
"order_type": "Shopping Cart",
|
||||
"party_name": get_party(frappe.session.user).name,
|
||||
"docstatus": 0,
|
||||
"contact_email": frappe.session.user,
|
||||
"selling_price_list": "_Test Price List Rest of the World",
|
||||
"currency": "USD",
|
||||
"taxes_and_charges": "_Test Tax 1 - _TC",
|
||||
"conversion_rate": 1,
|
||||
"transaction_date": nowdate(),
|
||||
"valid_till": add_months(nowdate(), 1),
|
||||
"items": [{"item_code": "_Test Item", "qty": 1}],
|
||||
"taxes": frappe.get_doc("Sales Taxes and Charges Template", "_Test Tax 1 - _TC").taxes,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
|
||||
quotation.update(values)
|
||||
|
||||
quotation.insert(ignore_permissions=True)
|
||||
|
||||
return quotation
|
||||
|
||||
def remove_test_quotation(self, quotation):
|
||||
frappe.set_user("Administrator")
|
||||
quotation.delete()
|
||||
|
||||
# helper functions
|
||||
def enable_shopping_cart(self):
|
||||
settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
|
||||
|
||||
settings.update(
|
||||
{
|
||||
"enabled": 1,
|
||||
"company": "_Test Company",
|
||||
"default_customer_group": "_Test Customer Group",
|
||||
"quotation_series": "_T-Quotation-",
|
||||
"price_list": "_Test Price List India",
|
||||
}
|
||||
)
|
||||
|
||||
# insert item price
|
||||
if not frappe.db.get_value(
|
||||
"Item Price", {"price_list": "_Test Price List India", "item_code": "_Test Item"}
|
||||
):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Item Price",
|
||||
"price_list": "_Test Price List India",
|
||||
"item_code": "_Test Item",
|
||||
"price_list_rate": 10,
|
||||
}
|
||||
).insert()
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Item Price",
|
||||
"price_list": "_Test Price List India",
|
||||
"item_code": "_Test Item 2",
|
||||
"price_list_rate": 20,
|
||||
}
|
||||
).insert()
|
||||
|
||||
settings.save()
|
||||
frappe.local.shopping_cart_settings = None
|
||||
|
||||
def disable_shopping_cart(self):
|
||||
settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
|
||||
settings.enabled = 0
|
||||
settings.save()
|
||||
frappe.local.shopping_cart_settings = None
|
||||
|
||||
def login_as_new_user(self):
|
||||
self.create_user_if_not_exists("test_cart_user@example.com")
|
||||
frappe.set_user("test_cart_user@example.com")
|
||||
|
||||
def login_as_customer(
|
||||
self, email="test_contact_customer@example.com", name="_Test Contact For _Test Customer"
|
||||
):
|
||||
self.create_user_if_not_exists(email, name)
|
||||
frappe.set_user(email)
|
||||
|
||||
def clear_existing_quotations(self):
|
||||
quotations = frappe.get_all(
|
||||
"Quotation",
|
||||
filters={"party_name": get_party().name, "order_type": "Shopping Cart", "docstatus": 0},
|
||||
order_by="modified desc",
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
for quotation in quotations:
|
||||
frappe.delete_doc("Quotation", quotation, ignore_permissions=True, force=True)
|
||||
|
||||
def create_user_if_not_exists(self, email, first_name=None):
|
||||
if frappe.db.exists("User", email):
|
||||
return
|
||||
|
||||
user = 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)
|
||||
|
||||
user.add_roles("Customer")
|
||||
|
||||
|
||||
def create_address_and_contact(**kwargs):
|
||||
if not frappe.db.get_value("Address", {"address_title": kwargs.get("address_title")}):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Address",
|
||||
"address_title": kwargs.get("address_title"),
|
||||
"address_type": kwargs.get("address_type") or "Office",
|
||||
"address_line1": kwargs.get("address_line1") or "Station Road",
|
||||
"city": kwargs.get("city") or "_Test City",
|
||||
"state": kwargs.get("state") or "Test State",
|
||||
"country": kwargs.get("country") or "India",
|
||||
"links": [
|
||||
{"link_doctype": "Customer", "link_name": kwargs.get("customer") or "_Test Customer"}
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
if not frappe.db.get_value("Contact", {"first_name": kwargs.get("first_name")}):
|
||||
contact = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Contact",
|
||||
"first_name": kwargs.get("first_name"),
|
||||
"links": [
|
||||
{"link_doctype": "Customer", "link_name": kwargs.get("customer") or "_Test Customer"}
|
||||
],
|
||||
}
|
||||
)
|
||||
contact.add_email(kwargs.get("email") or "test_contact_customer@example.com", is_primary=True)
|
||||
contact.add_phone(kwargs.get("phone") or "+91 0000000000", is_primary_phone=True)
|
||||
contact.insert()
|
||||
|
||||
|
||||
test_dependencies = [
|
||||
"Sales Taxes and Charges Template",
|
||||
"Price List",
|
||||
"Item Price",
|
||||
"Shipping Rule",
|
||||
"Currency Exchange",
|
||||
"Customer Group",
|
||||
"Lead",
|
||||
"Customer",
|
||||
"Contact",
|
||||
"Address",
|
||||
"Item",
|
||||
"Tax Rule",
|
||||
]
|
||||
@@ -1,54 +0,0 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
import frappe
|
||||
|
||||
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import is_cart_enabled
|
||||
|
||||
|
||||
def show_cart_count():
|
||||
if (
|
||||
is_cart_enabled()
|
||||
and frappe.db.get_value("User", frappe.session.user, "user_type") == "Website User"
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def set_cart_count(login_manager):
|
||||
# since this is run only on hooks login event
|
||||
# make sure user is already a customer
|
||||
# before trying to set cart count
|
||||
user_is_customer = is_customer()
|
||||
if not user_is_customer:
|
||||
return
|
||||
|
||||
if show_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)
|
||||
# cart count is calculated from this quotation's items
|
||||
set_cart_count()
|
||||
|
||||
|
||||
def clear_cart_count(login_manager):
|
||||
if show_cart_count():
|
||||
frappe.local.cookie_manager.delete_cookie("cart_count")
|
||||
|
||||
|
||||
def update_website_context(context):
|
||||
cart_enabled = is_cart_enabled()
|
||||
context["shopping_cart_enabled"] = cart_enabled
|
||||
|
||||
|
||||
def is_customer():
|
||||
if frappe.session.user and frappe.session.user != "Guest":
|
||||
contact_name = frappe.get_value("Contact", {"email_id": frappe.session.user})
|
||||
if contact_name:
|
||||
contact = frappe.get_doc("Contact", contact_name)
|
||||
for link in contact.links:
|
||||
if link.link_doctype == "Customer":
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -1,130 +0,0 @@
|
||||
import frappe
|
||||
|
||||
|
||||
class ItemVariantsCacheManager:
|
||||
def __init__(self, item_code):
|
||||
self.item_code = item_code
|
||||
|
||||
def get_item_variants_data(self):
|
||||
val = frappe.cache().hget("item_variants_data", self.item_code)
|
||||
|
||||
if not val:
|
||||
self.build_cache()
|
||||
|
||||
return frappe.cache().hget("item_variants_data", self.item_code)
|
||||
|
||||
def get_attribute_value_item_map(self):
|
||||
val = frappe.cache().hget("attribute_value_item_map", self.item_code)
|
||||
|
||||
if not val:
|
||||
self.build_cache()
|
||||
|
||||
return frappe.cache().hget("attribute_value_item_map", self.item_code)
|
||||
|
||||
def get_item_attribute_value_map(self):
|
||||
val = frappe.cache().hget("item_attribute_value_map", self.item_code)
|
||||
|
||||
if not val:
|
||||
self.build_cache()
|
||||
|
||||
return frappe.cache().hget("item_attribute_value_map", self.item_code)
|
||||
|
||||
def get_optional_attributes(self):
|
||||
val = frappe.cache().hget("optional_attributes", self.item_code)
|
||||
|
||||
if not val:
|
||||
self.build_cache()
|
||||
|
||||
return frappe.cache().hget("optional_attributes", self.item_code)
|
||||
|
||||
def get_ordered_attribute_values(self):
|
||||
val = frappe.cache().get_value("ordered_attribute_values_map")
|
||||
if val:
|
||||
return val
|
||||
|
||||
all_attribute_values = frappe.get_all(
|
||||
"Item Attribute Value", ["attribute_value", "idx", "parent"], order_by="idx asc"
|
||||
)
|
||||
|
||||
ordered_attribute_values_map = frappe._dict({})
|
||||
for d in all_attribute_values:
|
||||
ordered_attribute_values_map.setdefault(d.parent, []).append(d.attribute_value)
|
||||
|
||||
frappe.cache().set_value("ordered_attribute_values_map", ordered_attribute_values_map)
|
||||
return ordered_attribute_values_map
|
||||
|
||||
def build_cache(self):
|
||||
parent_item_code = self.item_code
|
||||
|
||||
attributes = [
|
||||
a.attribute
|
||||
for a in frappe.get_all(
|
||||
"Item Variant Attribute", {"parent": parent_item_code}, ["attribute"], order_by="idx asc"
|
||||
)
|
||||
]
|
||||
|
||||
# Get Variants and tehir Attributes that are not disabled
|
||||
iva = frappe.qb.DocType("Item Variant Attribute")
|
||||
item = frappe.qb.DocType("Item")
|
||||
query = (
|
||||
frappe.qb.from_(iva)
|
||||
.join(item)
|
||||
.on(item.name == iva.parent)
|
||||
.select(iva.parent, iva.attribute, iva.attribute_value)
|
||||
.where((iva.variant_of == parent_item_code) & (item.disabled == 0))
|
||||
.orderby(iva.name)
|
||||
)
|
||||
item_variants_data = query.run()
|
||||
|
||||
attribute_value_item_map = frappe._dict()
|
||||
item_attribute_value_map = frappe._dict()
|
||||
|
||||
for row in item_variants_data:
|
||||
item_code, attribute, attribute_value = row
|
||||
# (attr, value) => [item1, item2]
|
||||
attribute_value_item_map.setdefault((attribute, attribute_value), []).append(item_code)
|
||||
# item => {attr1: value1, attr2: value2}
|
||||
item_attribute_value_map.setdefault(item_code, {})[attribute] = attribute_value
|
||||
|
||||
optional_attributes = set()
|
||||
for item_code, attr_dict in item_attribute_value_map.items():
|
||||
for attribute in attributes:
|
||||
if attribute not in attr_dict:
|
||||
optional_attributes.add(attribute)
|
||||
|
||||
frappe.cache().hset("attribute_value_item_map", parent_item_code, attribute_value_item_map)
|
||||
frappe.cache().hset("item_attribute_value_map", parent_item_code, item_attribute_value_map)
|
||||
frappe.cache().hset("item_variants_data", parent_item_code, item_variants_data)
|
||||
frappe.cache().hset("optional_attributes", parent_item_code, optional_attributes)
|
||||
|
||||
def clear_cache(self):
|
||||
keys = [
|
||||
"attribute_value_item_map",
|
||||
"item_attribute_value_map",
|
||||
"item_variants_data",
|
||||
"optional_attributes",
|
||||
]
|
||||
|
||||
for key in keys:
|
||||
frappe.cache().hdel(key, self.item_code)
|
||||
|
||||
def rebuild_cache(self):
|
||||
self.clear_cache()
|
||||
enqueue_build_cache(self.item_code)
|
||||
|
||||
|
||||
def build_cache(item_code):
|
||||
frappe.cache().hset("item_cache_build_in_progress", item_code, 1)
|
||||
i = ItemVariantsCacheManager(item_code)
|
||||
i.build_cache()
|
||||
frappe.cache().hset("item_cache_build_in_progress", item_code, 0)
|
||||
|
||||
|
||||
def enqueue_build_cache(item_code):
|
||||
if frappe.cache().hget("item_cache_build_in_progress", item_code):
|
||||
return
|
||||
frappe.enqueue(
|
||||
"erpnext.e_commerce.variant_selector.item_variants_cache.build_cache",
|
||||
item_code=item_code,
|
||||
queue="long",
|
||||
)
|
||||
@@ -1,125 +0,0 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
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
|
||||
|
||||
test_dependencies = ["Item"]
|
||||
|
||||
|
||||
class TestVariantSelector(FrappeTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
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)
|
||||
|
||||
frappe.local.shopping_cart_settings = None # clear cached settings values
|
||||
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")
|
||||
@@ -1,251 +0,0 @@
|
||||
import frappe
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
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):
|
||||
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
|
||||
|
||||
"""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["is_stock_item"] = frappe.get_cached_value("Item", exact_match[0], "is_stock_item")
|
||||
product_info["allow_items_not_in_stock"] = cint(cart_settings.allow_items_not_in_stock)
|
||||
else:
|
||||
product_info = None
|
||||
|
||||
product_id = ""
|
||||
warehouse = ""
|
||||
if exact_match or filtered_items:
|
||||
if exact_match and len(exact_match) == 1:
|
||||
product_id = exact_match[0]
|
||||
elif filtered_items_count == 1:
|
||||
product_id = list(filtered_items)[0]
|
||||
|
||||
if product_id:
|
||||
warehouse = frappe.get_cached_value(
|
||||
"Website Item", {"item_code": product_id}, "website_warehouse"
|
||||
)
|
||||
|
||||
available_qty = 0.0
|
||||
if warehouse and frappe.get_cached_value("Warehouse", warehouse, "is_group") == 1:
|
||||
warehouses = get_child_warehouses(warehouse)
|
||||
else:
|
||||
warehouses = [warehouse] if warehouse else []
|
||||
|
||||
for warehouse in warehouses:
|
||||
available_qty += flt(
|
||||
frappe.db.get_value("Bin", {"item_code": product_id, "warehouse": warehouse}, "actual_qty")
|
||||
)
|
||||
|
||||
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,
|
||||
"available_qty": available_qty,
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
@@ -1,86 +0,0 @@
|
||||
{%- macro slide(image, title, subtitle, action, label, index, align="Left", theme="Dark") -%}
|
||||
{%- set align_class = resolve_class({
|
||||
'text-right': align == 'Right',
|
||||
'text-center': align == 'Centre',
|
||||
'text-left': align == 'Left',
|
||||
}) -%}
|
||||
|
||||
{%- set heading_class = resolve_class({
|
||||
'text-white': theme == 'Dark',
|
||||
'': theme == 'Light',
|
||||
}) -%}
|
||||
<div class="carousel-item {{ 'active' if index=='1' else ''}}" style="height: 450px;">
|
||||
<img class="d-block h-100 w-100" style="object-fit: cover;" src="{{ image }}" alt="{{ title }}">
|
||||
{%- if title or subtitle -%}
|
||||
<div class="carousel-body container d-flex {{ align_class }}">
|
||||
<div class="carousel-content align-self-center">
|
||||
{%- if title -%}<h1 class="{{ heading_class }}">{{ title }}</h1>{%- endif -%}
|
||||
{%- if subtitle -%}<p class="{{ heading_class }} mt-2">{{ subtitle }}</p>{%- endif -%}
|
||||
{%- if action -%}
|
||||
<a href="{{ action }}" class="btn btn-primary mt-3">
|
||||
{{ label }}
|
||||
</a>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- set hero_slider_id = 'id-' + frappe.utils.generate_hash('HeroSlider', 12) -%}
|
||||
|
||||
<div id="{{ hero_slider_id }}" class="section-carousel carousel slide" data-ride="carousel">
|
||||
{%- if show_indicators -%}
|
||||
<ol class="carousel-indicators">
|
||||
{%- for index in ['1', '2', '3', '4', '5'] -%}
|
||||
{%- if values['slide_' + index + '_image'] -%}
|
||||
<li data-target="#{{ hero_slider_id }}" data-slide-to="{{ frappe.utils.cint(index) - 1 }}" class="{{ 'active' if index=='1' else ''}}"></li>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
</ol>
|
||||
{%- endif -%}
|
||||
<div class="carousel-inner {{ resolve_class({'rounded-carousel': rounded }) }}">
|
||||
{%- for index in ['1', '2', '3', '4', '5'] -%}
|
||||
{%- set image = values['slide_' + index + '_image'] -%}
|
||||
{%- set title = values['slide_' + index + '_title'] -%}
|
||||
{%- set subtitle = values['slide_' + index + '_subtitle'] -%}
|
||||
{%- set primary_action = values['slide_' + index + '_primary_action'] -%}
|
||||
{%- set primary_action_label = values['slide_' + index + '_primary_action_label'] -%}
|
||||
{%- set align = values['slide_' + index + '_content_align'] -%}
|
||||
{%- set theme = values['slide_' + index + '_theme'] -%}
|
||||
|
||||
{%- if image -%}
|
||||
{{ slide(image, title, subtitle, primary_action, primary_action_label, index, align, theme) }}
|
||||
{%- endif -%}
|
||||
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
{%- if show_controls -%}
|
||||
<a class="carousel-control-prev" href="#{{ hero_slider_id }}" role="button" data-slide="prev">
|
||||
<div class="carousel-control">
|
||||
<svg class="mr-1" width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.625 3.75L6.375 9L11.625 14.25" stroke="#4C5A67" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
<a class="carousel-control-next" href="#{{ hero_slider_id }}" role="button" data-slide="next">
|
||||
<div class="carousel-control">
|
||||
<svg class="ml-1" width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.375 14.25L11.625 9L6.375 3.75" stroke="#4C5A67" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
frappe.ready(function () {
|
||||
$('.carousel').carousel({
|
||||
interval: false,
|
||||
pause: "hover",
|
||||
wrap: true
|
||||
})
|
||||
});
|
||||
</script>
|
||||
@@ -1,288 +0,0 @@
|
||||
{
|
||||
"__unsaved": 1,
|
||||
"creation": "2020-11-17 15:21:51.207221",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Template",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "slider_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Slider Name",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "show_indicators",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Indicators",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "show_controls",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Controls",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_1",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Slide 1",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_1_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Image",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_1_title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_1_subtitle",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Subtitle",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_1_primary_action_label",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action Label",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_1_primary_action",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_1_content_align",
|
||||
"fieldtype": "Select",
|
||||
"label": "Content Align",
|
||||
"options": "Left\nCentre\nRight",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_1_theme",
|
||||
"fieldtype": "Select",
|
||||
"label": "Slide Theme",
|
||||
"options": "Dark\nLight",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_2",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Slide 2",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_2_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Image ",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_2_title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title ",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_2_subtitle",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Subtitle ",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_2_primary_action_label",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action Label ",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_2_primary_action",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action ",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"default": "Left",
|
||||
"fieldname": "slide_2_content_align",
|
||||
"fieldtype": "Select",
|
||||
"label": "Content Align",
|
||||
"options": "Left\nCentre\nRight",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_2_theme",
|
||||
"fieldtype": "Select",
|
||||
"label": "Slide Theme",
|
||||
"options": "Dark\nLight",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_3",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Slide 3",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_3_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Image",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_3_title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_3_subtitle",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Subtitle",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_3_primary_action_label",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action Label",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_3_primary_action",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_3_content_align",
|
||||
"fieldtype": "Select",
|
||||
"label": "Content Align",
|
||||
"options": "Left\nCentre\nRight",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_3_theme",
|
||||
"fieldtype": "Select",
|
||||
"label": "Slide Theme",
|
||||
"options": "Dark\nLight",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_4",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Slide 4",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_4_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Image",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_4_title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_4_subtitle",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Subtitle",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_4_primary_action_label",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action Label",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_4_primary_action",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_4_content_align",
|
||||
"fieldtype": "Select",
|
||||
"label": "Content Align",
|
||||
"options": "Left\nCentre\nRight",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_4_theme",
|
||||
"fieldtype": "Select",
|
||||
"label": "Slide Theme",
|
||||
"options": "Dark\nLight",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_5",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Slide 5",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_5_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Image",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_5_title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_5_subtitle",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Subtitle",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_5_primary_action_label",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action Label",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_5_primary_action",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_5_content_align",
|
||||
"fieldtype": "Select",
|
||||
"label": "Content Align",
|
||||
"options": "Left\nCentre\nRight",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_5_theme",
|
||||
"fieldtype": "Select",
|
||||
"label": "Slide Theme",
|
||||
"options": "Dark\nLight",
|
||||
"reqd": 0
|
||||
}
|
||||
],
|
||||
"idx": 2,
|
||||
"modified": "2023-05-12 15:03:57.604060",
|
||||
"modified_by": "Administrator",
|
||||
"module": "E-commerce",
|
||||
"name": "Hero Slider",
|
||||
"owner": "Administrator",
|
||||
"standard": 1,
|
||||
"template": "",
|
||||
"type": "Section"
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
{% from "erpnext/templates/includes/macros.html" import item_card, item_card_body %}
|
||||
|
||||
<div class="section-with-cards item-card-group-section">
|
||||
<div class="item-group-header d-flex justify-content-between">
|
||||
<div class="title-section">
|
||||
{%- if title -%}
|
||||
<h2 class="section-title">{{ title }}</h2>
|
||||
{%- endif -%}
|
||||
{%- if subtitle -%}
|
||||
<p class="section-description">{{ subtitle }}</p>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
<div class="primary-action-section">
|
||||
{%- if primary_action -%}
|
||||
<a href="{{ action }}" class="btn btn-primary pull-right">
|
||||
{{ primary_action_label }}
|
||||
</a>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
{%- for index in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] -%}
|
||||
{%- set item = values['card_' + index + '_item'] -%}
|
||||
{%- if item -%}
|
||||
{%- set web_item = frappe.get_doc("Website Item", item) -%}
|
||||
{{ item_card(
|
||||
web_item, is_featured=values['card_' + index + '_featured'],
|
||||
is_full_width=True, align="Center"
|
||||
) }}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,270 +0,0 @@
|
||||
{
|
||||
"__unsaved": 1,
|
||||
"creation": "2020-11-17 15:35:05.285322",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Template",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subtitle",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subtitle",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "primary_action_label",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action Label",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "primary_action",
|
||||
"fieldtype": "Data",
|
||||
"label": "Primary Action",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_1",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 1",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_1_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_1_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_2",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 2",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_2_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_2_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_3",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 3",
|
||||
"options": "",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_3_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_3_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_4",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 4",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_4_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_4_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_5",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 5",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_5_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_5_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_6",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 6",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_6_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_6_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_7",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 7",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_7_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_7_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_8",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 8",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_8_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_8_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_9",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 9",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_9_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_9_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_10",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 10",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_10_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_10_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_11",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 11",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_11_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_11_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_12",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 12",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_12_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_12_featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"reqd": 0
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"modified": "2021-12-21 14:44:59.821335",
|
||||
"modified_by": "Administrator",
|
||||
"module": "E-commerce",
|
||||
"name": "Item Card Group",
|
||||
"owner": "Administrator",
|
||||
"standard": 1,
|
||||
"template": "",
|
||||
"type": "Section"
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"__unsaved": 1,
|
||||
"creation": "2020-11-17 15:28:47.809342",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Template",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "featured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Featured",
|
||||
"options": "",
|
||||
"reqd": 0
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"modified": "2021-02-24 16:05:17.926610",
|
||||
"modified_by": "Administrator",
|
||||
"module": "E-commerce",
|
||||
"name": "Product Card",
|
||||
"owner": "Administrator",
|
||||
"standard": 1,
|
||||
"template": "",
|
||||
"type": "Component"
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
{%- macro card(title, image, url, text_primary=False) -%}
|
||||
{%- set align_class = resolve_class({
|
||||
'text-right': text_primary,
|
||||
'text-centre': align == 'Center',
|
||||
'text-left': align == 'Left',
|
||||
}) -%}
|
||||
<div class="card h-100">
|
||||
{% if image %}
|
||||
<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>
|
||||
<a href="{{ url or '#' }}" class="stretched-link"></a>
|
||||
</div>
|
||||
{%- endmacro -%}
|
||||
|
||||
<div class="section-with-cards product-category-section">
|
||||
{%- if title -%}
|
||||
<h2 class="section-title">{{ title }}</h2>
|
||||
{%- endif -%}
|
||||
{%- if subtitle -%}
|
||||
<p class="section-description">{{ subtitle }}</p>
|
||||
{%- endif -%}
|
||||
<!-- {%- set card_size = card_size or 'Small' -%} -->
|
||||
<div class="{{ resolve_class({'mt-6': title}) }}">
|
||||
<div class="card-grid">
|
||||
{%- for index in ['1', '2', '3', '4', '5', '6', '7', '8'] -%}
|
||||
{%- set category = values['category_' + index] -%}
|
||||
{%- if category -%}
|
||||
{%- set category = frappe.get_doc("Item Group", category) -%}
|
||||
{{ card(category.name, category.image, category.route) }}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,85 +0,0 @@
|
||||
{
|
||||
"__unsaved": 1,
|
||||
"creation": "2020-11-17 15:25:50.855934",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Template",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subtitle",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subtitle",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "category_1",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item Group",
|
||||
"options": "Item Group",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "category_2",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item Group",
|
||||
"options": "Item Group",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "category_3",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item Group",
|
||||
"options": "Item Group",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "category_4",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item Group",
|
||||
"options": "Item Group",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "category_5",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item Group",
|
||||
"options": "Item Group",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "category_6",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item Group",
|
||||
"options": "Item Group",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "category_7",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item Group",
|
||||
"options": "Item Group",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "category_8",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item Group",
|
||||
"options": "Item Group",
|
||||
"reqd": 0
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"modified": "2021-02-24 16:03:33.835635",
|
||||
"modified_by": "Administrator",
|
||||
"module": "E-commerce",
|
||||
"name": "Product Category Cards",
|
||||
"owner": "Administrator",
|
||||
"standard": 1,
|
||||
"template": "",
|
||||
"type": "Section"
|
||||
}
|
||||
@@ -52,11 +52,7 @@ leaderboards = "erpnext.startup.leaderboard.get_leaderboards"
|
||||
filters_config = "erpnext.startup.filters.get_filters_config"
|
||||
additional_print_settings = "erpnext.controllers.print_settings.get_print_settings"
|
||||
|
||||
on_session_creation = [
|
||||
"erpnext.portal.utils.create_customer_or_supplier",
|
||||
"erpnext.e_commerce.shopping_cart.utils.set_cart_count",
|
||||
]
|
||||
on_logout = "erpnext.e_commerce.shopping_cart.utils.clear_cart_count"
|
||||
on_session_creation = "erpnext.portal.utils.create_customer_or_supplier"
|
||||
|
||||
treeviews = [
|
||||
"Account",
|
||||
@@ -90,15 +86,11 @@ jinja = {
|
||||
}
|
||||
|
||||
# website
|
||||
update_website_context = [
|
||||
"erpnext.e_commerce.shopping_cart.utils.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", "Sales Order", "Holiday List", "ToDo"]
|
||||
|
||||
website_generators = ["Item Group", "Website Item", "BOM", "Sales Partner"]
|
||||
website_generators = ["BOM", "Sales Partner"]
|
||||
|
||||
website_context = {
|
||||
"favicon": "/assets/erpnext/images/erpnext-favicon.svg",
|
||||
@@ -349,9 +341,6 @@ doc_events = {
|
||||
"Event": {
|
||||
"after_insert": "erpnext.crm.utils.link_events_with_prospect",
|
||||
},
|
||||
"Sales Taxes and Charges Template": {
|
||||
"on_update": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.validate_cart_settings"
|
||||
},
|
||||
"Sales Invoice": {
|
||||
"on_submit": [
|
||||
"erpnext.regional.create_transaction_log",
|
||||
|
||||
@@ -17,5 +17,4 @@ Quality Management
|
||||
Communication
|
||||
Telephony
|
||||
Bulk Transaction
|
||||
E-commerce
|
||||
Subcontracting
|
||||
Subcontracting
|
||||
|
||||
@@ -223,9 +223,6 @@ execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Catego
|
||||
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
|
||||
@@ -242,7 +239,6 @@ erpnext.patches.v12_0.update_production_plan_status
|
||||
erpnext.patches.v13_0.healthcare_deprecation_warning
|
||||
erpnext.patches.v13_0.item_naming_series_not_mandatory
|
||||
erpnext.patches.v13_0.update_category_in_ltds_certificate
|
||||
erpnext.patches.v13_0.fetch_thumbnail_in_website_items
|
||||
erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit
|
||||
erpnext.patches.v14_0.migrate_crm_settings
|
||||
erpnext.patches.v13_0.wipe_serial_no_field_for_0_qty
|
||||
@@ -257,6 +253,7 @@ erpnext.patches.v13_0.show_hr_payroll_deprecation_warning
|
||||
erpnext.patches.v13_0.reset_corrupt_defaults
|
||||
erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
|
||||
erpnext.patches.v15_0.delete_taxjar_doctypes
|
||||
erpnext.patches.v15_0.delete_ecommerce_doctypes
|
||||
erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets
|
||||
erpnext.patches.v14_0.update_reference_due_date_in_journal_entry
|
||||
erpnext.patches.v15_0.saudi_depreciation_warning
|
||||
@@ -277,8 +274,6 @@ erpnext.patches.v14_0.delete_datev_doctypes
|
||||
erpnext.patches.v14_0.rearrange_company_fields
|
||||
erpnext.patches.v13_0.update_sane_transfer_against
|
||||
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
|
||||
erpnext.patches.v13_0.update_reserved_qty_closed_wo
|
||||
erpnext.patches.v13_0.update_exchange_rate_settings
|
||||
erpnext.patches.v14_0.delete_amazon_mws_doctype
|
||||
@@ -288,7 +283,6 @@ erpnext.patches.v14_0.update_batch_valuation_flag
|
||||
erpnext.patches.v14_0.delete_non_profit_doctypes
|
||||
erpnext.patches.v13_0.set_return_against_in_pos_invoice_references
|
||||
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022
|
||||
erpnext.patches.v13_0.copy_custom_field_filters_to_website_item
|
||||
erpnext.patches.v13_0.change_default_item_manufacturer_fieldtype
|
||||
erpnext.patches.v13_0.requeue_recoverable_reposts
|
||||
erpnext.patches.v14_0.discount_accounting_separation
|
||||
@@ -346,4 +340,4 @@ erpnext.patches.v14_0.update_invoicing_period_in_subscription
|
||||
execute:frappe.delete_doc("Page", "welcome-to-erpnext")
|
||||
erpnext.patches.v15_0.delete_payment_gateway_doctypes
|
||||
# below migration patch should always run last
|
||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
@@ -1,60 +0,0 @@
|
||||
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:
|
||||
doc.log_error("Website Item creation failed")
|
||||
return None
|
||||
@@ -1,94 +0,0 @@
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
|
||||
|
||||
def execute():
|
||||
"Add Field Filters, that are not standard fields in Website Item, as Custom Fields."
|
||||
|
||||
def move_table_multiselect_data(docfield):
|
||||
"Copy child table data (Table Multiselect) from Item to Website Item for a docfield."
|
||||
table_multiselect_data = get_table_multiselect_data(docfield)
|
||||
field = docfield.fieldname
|
||||
|
||||
for row in table_multiselect_data:
|
||||
# add copied multiselect data rows in Website Item
|
||||
web_item = frappe.db.get_value("Website Item", {"item_code": row.parent})
|
||||
web_item_doc = frappe.get_doc("Website Item", web_item)
|
||||
|
||||
child_doc = frappe.new_doc(docfield.options, parent_doc=web_item_doc, parentfield=field)
|
||||
|
||||
for field in ["name", "creation", "modified", "idx"]:
|
||||
row[field] = None
|
||||
|
||||
child_doc.update(row)
|
||||
|
||||
child_doc.parenttype = "Website Item"
|
||||
child_doc.parent = web_item
|
||||
|
||||
child_doc.insert()
|
||||
|
||||
def get_table_multiselect_data(docfield):
|
||||
child_table = frappe.qb.DocType(docfield.options)
|
||||
item = frappe.qb.DocType("Item")
|
||||
|
||||
table_multiselect_data = ( # query table data for field
|
||||
frappe.qb.from_(child_table)
|
||||
.join(item)
|
||||
.on(item.item_code == child_table.parent)
|
||||
.select(child_table.star)
|
||||
.where((child_table.parentfield == docfield.fieldname) & (item.published_in_website == 1))
|
||||
).run(as_dict=True)
|
||||
|
||||
return table_multiselect_data
|
||||
|
||||
settings = frappe.get_doc("E Commerce Settings")
|
||||
|
||||
if not (settings.enable_field_filters or settings.filter_fields):
|
||||
return
|
||||
|
||||
item_meta = frappe.get_meta("Item")
|
||||
valid_item_fields = [
|
||||
df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]
|
||||
]
|
||||
|
||||
web_item_meta = frappe.get_meta("Website Item")
|
||||
valid_web_item_fields = [
|
||||
df.fieldname for df in web_item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]
|
||||
]
|
||||
|
||||
for row in settings.filter_fields:
|
||||
# skip if illegal field
|
||||
if row.fieldname not in valid_item_fields:
|
||||
continue
|
||||
|
||||
# if Item field is not in Website Item, add it as a custom field
|
||||
if row.fieldname not in valid_web_item_fields:
|
||||
df = item_meta.get_field(row.fieldname)
|
||||
create_custom_field(
|
||||
"Website Item",
|
||||
dict(
|
||||
owner="Administrator",
|
||||
fieldname=df.fieldname,
|
||||
label=df.label,
|
||||
fieldtype=df.fieldtype,
|
||||
options=df.options,
|
||||
description=df.description,
|
||||
read_only=df.read_only,
|
||||
no_copy=df.no_copy,
|
||||
insert_after="on_backorder",
|
||||
),
|
||||
)
|
||||
|
||||
# map field values
|
||||
if df.fieldtype == "Table MultiSelect":
|
||||
move_table_multiselect_data(df)
|
||||
else:
|
||||
frappe.db.sql( # nosemgrep
|
||||
"""
|
||||
UPDATE `tabWebsite Item` wi, `tabItem` i
|
||||
SET wi.{0} = i.{0}
|
||||
WHERE wi.item_code = i.item_code
|
||||
""".format(
|
||||
row.fieldname
|
||||
)
|
||||
)
|
||||
@@ -1,85 +0,0 @@
|
||||
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",
|
||||
"has_variants",
|
||||
"variant_of",
|
||||
"description",
|
||||
"weightage",
|
||||
]
|
||||
web_fields_to_map = [
|
||||
"route",
|
||||
"slideshow",
|
||||
"website_image_alt",
|
||||
"website_warehouse",
|
||||
"web_long_description",
|
||||
"website_content",
|
||||
"website_image",
|
||||
"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)
|
||||
@@ -1,11 +0,0 @@
|
||||
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()
|
||||
@@ -1,15 +0,0 @@
|
||||
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()
|
||||
@@ -1,68 +0,0 @@
|
||||
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,
|
||||
)
|
||||
@@ -1,29 +0,0 @@
|
||||
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)
|
||||
@@ -11,6 +11,9 @@ def execute():
|
||||
asset_depreciation_schedules_map = get_asset_depreciation_schedules_map()
|
||||
|
||||
for asset in assets:
|
||||
if not asset_depreciation_schedules_map.get(asset.name):
|
||||
continue
|
||||
|
||||
depreciation_schedules = asset_depreciation_schedules_map[asset.name]
|
||||
|
||||
for fb_row in asset_finance_books_map[asset.name]:
|
||||
|
||||
30
erpnext/patches/v15_0/delete_ecommerce_doctypes.py
Normal file
30
erpnext/patches/v15_0/delete_ecommerce_doctypes.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import click
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
if "webshop" in frappe.get_installed_apps():
|
||||
return
|
||||
|
||||
if not frappe.db.table_exists("Website Item"):
|
||||
return
|
||||
|
||||
doctypes = [
|
||||
"E Commerce Settings",
|
||||
"Website Item",
|
||||
"Recommended Items",
|
||||
"Item Review",
|
||||
"Wishlist Item",
|
||||
"Wishlist",
|
||||
"Website Offer",
|
||||
"Website Item Tabbed Section",
|
||||
]
|
||||
|
||||
for doctype in doctypes:
|
||||
frappe.delete_doc("DocType", doctype, ignore_missing=True)
|
||||
|
||||
click.secho(
|
||||
"ECommerce is renamed and moved to a separate app"
|
||||
"Please install the app for ECommerce features: https://github.com/frappe/webshop",
|
||||
fg="yellow",
|
||||
)
|
||||
@@ -19,12 +19,3 @@ 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 && child.route) {
|
||||
window.open('/' + child.route, '_blank');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,10 +15,7 @@
|
||||
"description",
|
||||
"hero_image",
|
||||
"slideshow",
|
||||
"hero_section",
|
||||
"products_section",
|
||||
"products_url",
|
||||
"products"
|
||||
"hero_section"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -86,30 +83,11 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Homepage Section",
|
||||
"options": "Homepage Section"
|
||||
},
|
||||
{
|
||||
"fieldname": "products_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Products"
|
||||
},
|
||||
{
|
||||
"default": "/all-products",
|
||||
"fieldname": "products_url",
|
||||
"fieldtype": "Data",
|
||||
"label": "URL for \"All Products\""
|
||||
},
|
||||
{
|
||||
"description": "Products to be shown on website homepage",
|
||||
"fieldname": "products",
|
||||
"fieldtype": "Table",
|
||||
"label": "Products",
|
||||
"options": "Homepage Featured Product",
|
||||
"width": "40px"
|
||||
}
|
||||
],
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-18 13:29:29.531639",
|
||||
"modified": "2022-12-19 21:10:29.127277",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Portal",
|
||||
"name": "Homepage",
|
||||
@@ -138,6 +116,7 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "company",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -12,26 +12,3 @@ class Homepage(Document):
|
||||
if not self.description:
|
||||
self.description = frappe._("This is an example website auto-generated from ERPNext")
|
||||
delete_page_cache("home")
|
||||
|
||||
def setup_items(self):
|
||||
for d in frappe.get_all(
|
||||
"Website Item",
|
||||
fields=["name", "item_name", "description", "website_image", "route"],
|
||||
filters={"published": 1},
|
||||
limit=3,
|
||||
):
|
||||
|
||||
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.website_image,
|
||||
route=d.route,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2016-04-22 05:57:06.261401",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"item_code",
|
||||
"col_break1",
|
||||
"item_name",
|
||||
"view",
|
||||
"section_break_5",
|
||||
"description",
|
||||
"column_break_7",
|
||||
"image",
|
||||
"thumbnail",
|
||||
"route"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Item",
|
||||
"oldfieldname": "item_code",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Website Item",
|
||||
"print_width": "150px",
|
||||
"reqd": 1,
|
||||
"search_index": 1,
|
||||
"width": "150px"
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break1",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.item_name",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Item Name",
|
||||
"oldfieldname": "item_name",
|
||||
"oldfieldtype": "Data",
|
||||
"print_hide": 1,
|
||||
"print_width": "150",
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"width": "150"
|
||||
},
|
||||
{
|
||||
"fieldname": "view",
|
||||
"fieldtype": "Button",
|
||||
"in_list_view": 1,
|
||||
"label": "View"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Details"
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.web_long_description",
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Description",
|
||||
"oldfieldname": "description",
|
||||
"oldfieldtype": "Small Text",
|
||||
"print_width": "300px",
|
||||
"width": "300px"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_7",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.website_image",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach Image",
|
||||
"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",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-18 13:05:50.669311",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Portal",
|
||||
"name": "Homepage Featured Product",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class HomepageFeaturedProduct(Document):
|
||||
pass
|
||||
@@ -1,10 +1,4 @@
|
||||
import frappe
|
||||
from frappe.utils.nestedset import get_root_of
|
||||
|
||||
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||
get_shopping_cart_settings,
|
||||
)
|
||||
from erpnext.e_commerce.shopping_cart.cart import get_debtors_account
|
||||
|
||||
|
||||
def set_default_role(doc, method):
|
||||
@@ -56,26 +50,7 @@ def create_customer_or_supplier():
|
||||
party = frappe.new_doc(doctype)
|
||||
fullname = frappe.utils.get_fullname(user)
|
||||
|
||||
if doctype == "Customer":
|
||||
cart_settings = get_shopping_cart_settings()
|
||||
|
||||
if cart_settings.enable_checkout:
|
||||
debtors_account = get_debtors_account(cart_settings)
|
||||
else:
|
||||
debtors_account = ""
|
||||
|
||||
party.update(
|
||||
{
|
||||
"customer_name": fullname,
|
||||
"customer_type": "Individual",
|
||||
"customer_group": cart_settings.default_customer_group,
|
||||
"territory": get_root_of("Territory"),
|
||||
}
|
||||
)
|
||||
|
||||
if debtors_account:
|
||||
party.update({"accounts": [{"company": cart_settings.company, "account": debtors_account}]})
|
||||
else:
|
||||
if not doctype == "Customer":
|
||||
party.update(
|
||||
{
|
||||
"supplier_name": fullname,
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
},
|
||||
{
|
||||
"fetch_from": "task.subject",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "subject",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
@@ -31,7 +32,6 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "task.project",
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Text",
|
||||
"label": "Project",
|
||||
@@ -40,7 +40,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-09 11:34:14.335853",
|
||||
"modified": "2023-10-17 12:45:21.536165",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Task Depends On",
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
$(() => {
|
||||
class CustomerReviews {
|
||||
constructor() {
|
||||
this.bind_button_actions();
|
||||
this.start = 0;
|
||||
this.page_length = 10;
|
||||
}
|
||||
|
||||
bind_button_actions() {
|
||||
this.write_review();
|
||||
this.view_more();
|
||||
}
|
||||
|
||||
write_review() {
|
||||
//TODO: make dialog popup on stray page
|
||||
$('.page_content').on('click', '.btn-write-review', (e) => {
|
||||
// Bind action on write a review button
|
||||
const $btn = $(e.currentTarget);
|
||||
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("Write a Review"),
|
||||
fields: [
|
||||
{fieldname: "title", fieldtype: "Data", label: "Headline", reqd: 1},
|
||||
{fieldname: "rating", fieldtype: "Rating", label: "Overall Rating", reqd: 1},
|
||||
{fieldtype: "Section Break"},
|
||||
{fieldname: "comment", fieldtype: "Small Text", label: "Your Review"}
|
||||
],
|
||||
primary_action: function() {
|
||||
let data = d.get_values();
|
||||
frappe.call({
|
||||
method: "erpnext.e_commerce.doctype.item_review.item_review.add_item_review",
|
||||
args: {
|
||||
web_item: $btn.attr('data-web-item'),
|
||||
title: data.title,
|
||||
rating: data.rating,
|
||||
comment: data.comment
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __("Submitting Review ..."),
|
||||
callback: (r) => {
|
||||
if (!r.exc) {
|
||||
frappe.msgprint({
|
||||
message: __("Thank you for submitting your review"),
|
||||
title: __("Review Submitted"),
|
||||
indicator: "green"
|
||||
});
|
||||
d.hide();
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
primary_action_label: __('Submit')
|
||||
});
|
||||
d.show();
|
||||
});
|
||||
}
|
||||
|
||||
view_more() {
|
||||
$('.page_content').on('click', '.btn-view-more', (e) => {
|
||||
// Bind action on view more button
|
||||
const $btn = $(e.currentTarget);
|
||||
$btn.prop('disabled', true);
|
||||
|
||||
this.start += this.page_length;
|
||||
let me = this;
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.e_commerce.doctype.item_review.item_review.get_item_reviews",
|
||||
args: {
|
||||
web_item: $btn.attr('data-web-item'),
|
||||
start: me.start,
|
||||
end: me.page_length
|
||||
},
|
||||
callback: (result) => {
|
||||
if (result.message) {
|
||||
let res = result.message;
|
||||
me.get_user_review_html(res.reviews);
|
||||
|
||||
$btn.prop('disabled', false);
|
||||
if (res.total_reviews <= (me.start + me.page_length)) {
|
||||
$btn.hide();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
get_user_review_html(reviews) {
|
||||
let me = this;
|
||||
let $content = $('.user-reviews');
|
||||
|
||||
reviews.forEach((review) => {
|
||||
$content.append(`
|
||||
<div class="mb-3 review">
|
||||
<div class="d-flex">
|
||||
<p class="mr-4 user-review-title">
|
||||
<span>${__(review.review_title)}</span>
|
||||
</p>
|
||||
<div class="rating">
|
||||
${me.get_review_stars(review.rating)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="product-description mb-4">
|
||||
<p>
|
||||
${__(review.comment)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="review-signature mb-2">
|
||||
<span class="reviewer">${__(review.customer)}</span>
|
||||
<span class="indicator grey" style="--text-on-gray: var(--gray-300);"></span>
|
||||
<span class="reviewer">${__(review.published_on)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
}
|
||||
|
||||
get_review_stars(rating) {
|
||||
let stars = ``;
|
||||
for (let i = 1; i < 6; i++) {
|
||||
let fill_class = i <= rating ? 'star-click' : '';
|
||||
stars += `
|
||||
<svg class="icon icon-sm ${fill_class}">
|
||||
<use href="#icon-star"></use>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
return stars;
|
||||
}
|
||||
}
|
||||
|
||||
new CustomerReviews();
|
||||
});
|
||||
@@ -1,8 +1 @@
|
||||
import "./website_utils";
|
||||
import "./wishlist";
|
||||
import "./shopping_cart";
|
||||
import "./customer_reviews";
|
||||
import "../../e_commerce/product_ui/list";
|
||||
import "../../e_commerce/product_ui/views";
|
||||
import "../../e_commerce/product_ui/grid";
|
||||
import "../../e_commerce/product_ui/search";
|
||||
@@ -1,243 +0,0 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
// shopping cart
|
||||
frappe.provide("erpnext.e_commerce.shopping_cart");
|
||||
var shopping_cart = erpnext.e_commerce.shopping_cart;
|
||||
|
||||
var getParams = function (url) {
|
||||
var params = [];
|
||||
var parser = document.createElement('a');
|
||||
parser.href = url;
|
||||
var query = parser.search.substring(1);
|
||||
var vars = query.split('&');
|
||||
for (var i = 0; i < vars.length; i++) {
|
||||
var pair = vars[i].split('=');
|
||||
params[pair[0]] = decodeURIComponent(pair[1]);
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
frappe.ready(function() {
|
||||
var full_name = frappe.session && frappe.session.user_fullname;
|
||||
// update user
|
||||
if(full_name) {
|
||||
$('.navbar li[data-label="User"] a')
|
||||
.html('<i class="fa fa-fixed-width fa fa-user"></i> ' + full_name);
|
||||
}
|
||||
// set coupon code and sales partner code
|
||||
|
||||
var url_args = getParams(window.location.href);
|
||||
|
||||
var referral_coupon_code = url_args['cc'];
|
||||
var referral_sales_partner = url_args['sp'];
|
||||
|
||||
var d = new Date();
|
||||
// expires within 30 minutes
|
||||
d.setTime(d.getTime() + (0.02 * 24 * 60 * 60 * 1000));
|
||||
var expires = "expires="+d.toUTCString();
|
||||
if (referral_coupon_code) {
|
||||
document.cookie = "referral_coupon_code=" + referral_coupon_code + ";" + expires + ";path=/";
|
||||
}
|
||||
if (referral_sales_partner) {
|
||||
document.cookie = "referral_sales_partner=" + referral_sales_partner + ";" + expires + ";path=/";
|
||||
}
|
||||
referral_coupon_code=frappe.get_cookie("referral_coupon_code");
|
||||
referral_sales_partner=frappe.get_cookie("referral_sales_partner");
|
||||
|
||||
if (referral_coupon_code && $(".tot_quotation_discount").val()==undefined ) {
|
||||
$(".txtcoupon").val(referral_coupon_code);
|
||||
}
|
||||
if (referral_sales_partner) {
|
||||
$(".txtreferral_sales_partner").val(referral_sales_partner);
|
||||
}
|
||||
|
||||
// update login
|
||||
shopping_cart.show_shoppingcart_dropdown();
|
||||
shopping_cart.set_cart_count();
|
||||
shopping_cart.show_cart_navbar();
|
||||
});
|
||||
|
||||
$.extend(shopping_cart, {
|
||||
show_shoppingcart_dropdown: function() {
|
||||
$(".shopping-cart").on('shown.bs.dropdown', function() {
|
||||
if (!$('.shopping-cart-menu .cart-container').length) {
|
||||
return frappe.call({
|
||||
method: 'erpnext.e_commerce.shopping_cart.cart.get_shopping_cart_menu',
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
$('.shopping-cart-menu').html(r.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
update_cart: function(opts) {
|
||||
if (frappe.session.user==="Guest") {
|
||||
if (localStorage) {
|
||||
localStorage.setItem("last_visited", window.location.pathname);
|
||||
}
|
||||
frappe.call('erpnext.e_commerce.api.get_guest_redirect_on_action').then((res) => {
|
||||
window.location.href = res.message || "/login";
|
||||
});
|
||||
} else {
|
||||
shopping_cart.freeze();
|
||||
return frappe.call({
|
||||
type: "POST",
|
||||
method: "erpnext.e_commerce.shopping_cart.cart.update_cart",
|
||||
args: {
|
||||
item_code: opts.item_code,
|
||||
qty: opts.qty,
|
||||
additional_notes: opts.additional_notes !== undefined ? opts.additional_notes : undefined,
|
||||
with_items: opts.with_items || 0
|
||||
},
|
||||
btn: opts.btn,
|
||||
callback: function(r) {
|
||||
shopping_cart.unfreeze();
|
||||
shopping_cart.set_cart_count(true);
|
||||
if(opts.callback)
|
||||
opts.callback(r);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
set_cart_count: function(animate=false) {
|
||||
$(".intermediate-empty-cart").remove();
|
||||
|
||||
var cart_count = frappe.get_cookie("cart_count");
|
||||
if(frappe.session.user==="Guest") {
|
||||
cart_count = 0;
|
||||
}
|
||||
|
||||
if(cart_count) {
|
||||
$(".shopping-cart").toggleClass('hidden', false);
|
||||
}
|
||||
|
||||
var $cart = $('.cart-icon');
|
||||
var $badge = $cart.find("#cart-count");
|
||||
|
||||
if(parseInt(cart_count) === 0 || cart_count === undefined) {
|
||||
$cart.css("display", "none");
|
||||
$(".cart-tax-items").hide();
|
||||
$(".btn-place-order").hide();
|
||||
$(".cart-payment-addresses").hide();
|
||||
|
||||
let intermediate_empty_cart_msg = `
|
||||
<div class="text-center w-100 intermediate-empty-cart mt-4 mb-4 text-muted">
|
||||
${ __("Cart is Empty") }
|
||||
</div>
|
||||
`;
|
||||
$(".cart-table").after(intermediate_empty_cart_msg);
|
||||
}
|
||||
else {
|
||||
$cart.css("display", "inline");
|
||||
$("#cart-count").text(cart_count);
|
||||
}
|
||||
|
||||
if(cart_count) {
|
||||
$badge.html(cart_count);
|
||||
|
||||
if (animate) {
|
||||
$cart.addClass("cart-animate");
|
||||
setTimeout(() => {
|
||||
$cart.removeClass("cart-animate");
|
||||
}, 500);
|
||||
}
|
||||
} else {
|
||||
$badge.remove();
|
||||
}
|
||||
},
|
||||
|
||||
shopping_cart_update: function({item_code, qty, cart_dropdown, additional_notes}) {
|
||||
shopping_cart.update_cart({
|
||||
item_code,
|
||||
qty,
|
||||
additional_notes,
|
||||
with_items: 1,
|
||||
btn: this,
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
$(".cart-items").html(r.message.items);
|
||||
$(".cart-tax-items").html(r.message.total);
|
||||
$(".payment-summary").html(r.message.taxes_and_totals);
|
||||
shopping_cart.set_cart_count();
|
||||
|
||||
if (cart_dropdown != true) {
|
||||
$(".cart-icon").hide();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
show_cart_navbar: function () {
|
||||
frappe.call({
|
||||
method: "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.is_cart_enabled",
|
||||
callback: function(r) {
|
||||
$(".shopping-cart").toggleClass('hidden', r.message ? false : true);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
toggle_button_class(button, remove, add) {
|
||||
button.removeClass(remove);
|
||||
button.addClass(add);
|
||||
},
|
||||
|
||||
bind_add_to_cart_action() {
|
||||
$('.page_content').on('click', '.btn-add-to-cart-list', (e) => {
|
||||
const $btn = $(e.currentTarget);
|
||||
$btn.prop('disabled', true);
|
||||
|
||||
if (frappe.session.user==="Guest") {
|
||||
if (localStorage) {
|
||||
localStorage.setItem("last_visited", window.location.pathname);
|
||||
}
|
||||
frappe.call('erpnext.e_commerce.api.get_guest_redirect_on_action').then((res) => {
|
||||
window.location.href = res.message || "/login";
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
$btn.addClass('hidden');
|
||||
$btn.closest('.cart-action-container').addClass('d-flex');
|
||||
$btn.parent().find('.go-to-cart').removeClass('hidden');
|
||||
$btn.parent().find('.go-to-cart-grid').removeClass('hidden');
|
||||
$btn.parent().find('.cart-indicator').removeClass('hidden');
|
||||
|
||||
const item_code = $btn.data('item-code');
|
||||
erpnext.e_commerce.shopping_cart.update_cart({
|
||||
item_code,
|
||||
qty: 1
|
||||
});
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
freeze() {
|
||||
if (window.location.pathname !== "/cart") return;
|
||||
|
||||
if (!$('#freeze').length) {
|
||||
let freeze = $('<div id="freeze" class="modal-backdrop fade"></div>')
|
||||
.appendTo("body");
|
||||
|
||||
setTimeout(function() {
|
||||
freeze.addClass("show");
|
||||
}, 1);
|
||||
} else {
|
||||
$("#freeze").addClass("show");
|
||||
}
|
||||
},
|
||||
|
||||
unfreeze() {
|
||||
if ($('#freeze').length) {
|
||||
let freeze = $('#freeze').removeClass("show");
|
||||
setTimeout(function() {
|
||||
freeze.remove();
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,204 +0,0 @@
|
||||
frappe.provide("erpnext.e_commerce.wishlist");
|
||||
var wishlist = erpnext.e_commerce.wishlist;
|
||||
|
||||
frappe.provide("erpnext.e_commerce.shopping_cart");
|
||||
var shopping_cart = erpnext.e_commerce.shopping_cart;
|
||||
|
||||
$.extend(wishlist, {
|
||||
set_wishlist_count: function(animate=false) {
|
||||
// set badge count for wishlist icon
|
||||
var wish_count = frappe.get_cookie("wish_count");
|
||||
if (frappe.session.user==="Guest") {
|
||||
wish_count = 0;
|
||||
}
|
||||
|
||||
if (wish_count) {
|
||||
$(".wishlist").toggleClass('hidden', false);
|
||||
}
|
||||
|
||||
var $wishlist = $('.wishlist-icon');
|
||||
var $badge = $wishlist.find("#wish-count");
|
||||
|
||||
if (parseInt(wish_count) === 0 || wish_count === undefined) {
|
||||
$wishlist.css("display", "none");
|
||||
} else {
|
||||
$wishlist.css("display", "inline");
|
||||
}
|
||||
if (wish_count) {
|
||||
$badge.html(wish_count);
|
||||
if (animate) {
|
||||
$wishlist.addClass('cart-animate');
|
||||
setTimeout(() => {
|
||||
$wishlist.removeClass('cart-animate');
|
||||
}, 500);
|
||||
}
|
||||
} else {
|
||||
$badge.remove();
|
||||
}
|
||||
},
|
||||
|
||||
bind_move_to_cart_action: function() {
|
||||
// move item to cart from wishlist
|
||||
$('.page_content').on("click", ".btn-add-to-cart", (e) => {
|
||||
const $move_to_cart_btn = $(e.currentTarget);
|
||||
let item_code = $move_to_cart_btn.data("item-code");
|
||||
|
||||
shopping_cart.shopping_cart_update({
|
||||
item_code,
|
||||
qty: 1,
|
||||
cart_dropdown: true
|
||||
});
|
||||
|
||||
let success_action = function() {
|
||||
const $card_wrapper = $move_to_cart_btn.closest(".wishlist-card");
|
||||
$card_wrapper.addClass("wish-removed");
|
||||
};
|
||||
let args = { item_code: item_code };
|
||||
this.add_remove_from_wishlist("remove", args, success_action, null, true);
|
||||
});
|
||||
},
|
||||
|
||||
bind_remove_action: function() {
|
||||
// remove item from wishlist
|
||||
let me = this;
|
||||
|
||||
$('.page_content').on("click", ".remove-wish", (e) => {
|
||||
const $remove_wish_btn = $(e.currentTarget);
|
||||
let item_code = $remove_wish_btn.data("item-code");
|
||||
|
||||
let success_action = function() {
|
||||
const $card_wrapper = $remove_wish_btn.closest(".wishlist-card");
|
||||
$card_wrapper.addClass("wish-removed");
|
||||
if (frappe.get_cookie("wish_count") == 0) {
|
||||
$(".page_content").empty();
|
||||
me.render_empty_state();
|
||||
}
|
||||
};
|
||||
let args = { item_code: item_code };
|
||||
this.add_remove_from_wishlist("remove", args, success_action);
|
||||
});
|
||||
},
|
||||
|
||||
bind_wishlist_action() {
|
||||
// 'wish'('like') or 'unwish' item in product listing
|
||||
$('.page_content').on('click', '.like-action, .like-action-list', (e) => {
|
||||
const $btn = $(e.currentTarget);
|
||||
this.wishlist_action($btn);
|
||||
});
|
||||
},
|
||||
|
||||
wishlist_action(btn) {
|
||||
const $wish_icon = btn.find('.wish-icon');
|
||||
let me = this;
|
||||
|
||||
if (frappe.session.user==="Guest") {
|
||||
if (localStorage) {
|
||||
localStorage.setItem("last_visited", window.location.pathname);
|
||||
}
|
||||
this.redirect_guest();
|
||||
return;
|
||||
}
|
||||
|
||||
let success_action = function() {
|
||||
erpnext.e_commerce.wishlist.set_wishlist_count(true);
|
||||
};
|
||||
|
||||
if ($wish_icon.hasClass('wished')) {
|
||||
// un-wish item
|
||||
btn.removeClass("like-animate");
|
||||
btn.addClass("like-action-wished");
|
||||
this.toggle_button_class($wish_icon, 'wished', 'not-wished');
|
||||
|
||||
let args = { item_code: btn.data('item-code') };
|
||||
let failure_action = function() {
|
||||
me.toggle_button_class($wish_icon, 'not-wished', 'wished');
|
||||
};
|
||||
this.add_remove_from_wishlist("remove", args, success_action, failure_action);
|
||||
} else {
|
||||
// wish item
|
||||
btn.addClass("like-animate");
|
||||
btn.addClass("like-action-wished");
|
||||
this.toggle_button_class($wish_icon, 'not-wished', 'wished');
|
||||
|
||||
let args = {item_code: btn.data('item-code')};
|
||||
let failure_action = function() {
|
||||
me.toggle_button_class($wish_icon, 'wished', 'not-wished');
|
||||
};
|
||||
this.add_remove_from_wishlist("add", args, success_action, failure_action);
|
||||
}
|
||||
},
|
||||
|
||||
toggle_button_class(button, remove, add) {
|
||||
button.removeClass(remove);
|
||||
button.addClass(add);
|
||||
},
|
||||
|
||||
add_remove_from_wishlist(action, args, success_action, failure_action, async=false) {
|
||||
/* AJAX call to add or remove Item from Wishlist
|
||||
action: "add" or "remove"
|
||||
args: args for method (item_code, price, formatted_price),
|
||||
success_action: method to execute on successs,
|
||||
failure_action: method to execute on failure,
|
||||
async: make call asynchronously (true/false). */
|
||||
if (frappe.session.user==="Guest") {
|
||||
if (localStorage) {
|
||||
localStorage.setItem("last_visited", window.location.pathname);
|
||||
}
|
||||
this.redirect_guest();
|
||||
} else {
|
||||
let method = "erpnext.e_commerce.doctype.wishlist.wishlist.add_to_wishlist";
|
||||
if (action === "remove") {
|
||||
method = "erpnext.e_commerce.doctype.wishlist.wishlist.remove_from_wishlist";
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
async: async,
|
||||
type: "POST",
|
||||
method: method,
|
||||
args: args,
|
||||
callback: function (r) {
|
||||
if (r.exc) {
|
||||
if (failure_action && (typeof failure_action === 'function')) {
|
||||
failure_action();
|
||||
}
|
||||
frappe.msgprint({
|
||||
message: __("Sorry, something went wrong. Please refresh."),
|
||||
indicator: "red", title: __("Note")
|
||||
});
|
||||
} else if (success_action && (typeof success_action === 'function')) {
|
||||
success_action();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
redirect_guest() {
|
||||
frappe.call('erpnext.e_commerce.api.get_guest_redirect_on_action').then((res) => {
|
||||
window.location.href = res.message || "/login";
|
||||
});
|
||||
},
|
||||
|
||||
render_empty_state() {
|
||||
$(".page_content").append(`
|
||||
<div class="cart-empty frappe-card">
|
||||
<div class="cart-empty-state">
|
||||
<img src="/assets/erpnext/images/ui-states/cart-empty-state.png" alt="Empty Cart">
|
||||
</div>
|
||||
<div class="cart-empty-message mt-4">${ __('Wishlist is empty !') }</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
frappe.ready(function() {
|
||||
if (window.location.pathname !== "/wishlist") {
|
||||
$(".wishlist").toggleClass('hidden', true);
|
||||
wishlist.set_wishlist_count();
|
||||
} else {
|
||||
wishlist.bind_move_to_cart_action();
|
||||
wishlist.bind_remove_action();
|
||||
}
|
||||
|
||||
});
|
||||
@@ -1,2 +1 @@
|
||||
@import "./shopping_cart";
|
||||
@import "./website";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user