Merge branch 'develop' of https://github.com/frappe/erpnext into sales_order_item_dimensions

This commit is contained in:
Deepesh Garg 2023-10-17 18:30:49 +05:30
commit 88be7ada33
160 changed files with 630 additions and 15222 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"] = ""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,5 +17,4 @@ Quality Management
Communication
Telephony
Bulk Transaction
E-commerce
Subcontracting
Subcontracting

View File

@ -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
@ -347,4 +341,4 @@ execute:frappe.delete_doc("Page", "welcome-to-erpnext")
erpnext.patches.v15_0.delete_payment_gateway_doctypes
erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item
# 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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",
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +1 @@
@import "./shopping_cart";
@import "./website";

File diff suppressed because it is too large Load Diff

View File

@ -26,7 +26,6 @@ class Quotation(SellingController):
self.set_status()
self.validate_uom_is_integer("stock_uom", "qty")
self.validate_valid_till()
self.validate_shopping_cart_items()
self.set_customer_name()
if self.items:
self.with_items = 1
@ -42,26 +41,6 @@ class Quotation(SellingController):
if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date):
frappe.throw(_("Valid till date cannot be before transaction date"))
def validate_shopping_cart_items(self):
if self.order_type != "Shopping Cart":
return
for item in self.items:
has_web_item = frappe.db.exists("Website Item", {"item_code": item.item_code})
# If variant is unpublished but template is published: valid
template = frappe.get_cached_value("Item", item.item_code, "variant_of")
if template and not has_web_item:
has_web_item = frappe.db.exists("Website Item", {"item_code": template})
if not has_web_item:
frappe.throw(
_("Row #{0}: Item {1} must have a Website Item for Shopping Cart Quotations").format(
item.idx, frappe.bold(item.item_code)
),
title=_("Unpublished Item"),
)
def set_has_alternative_item(self):
"""Mark 'Has Alternative Item' for rows."""
if not any(row.is_alternative for row in self.get("items")):
@ -263,8 +242,8 @@ def make_sales_order(source_name: str, target_doc=None):
return _make_sales_order(source_name, target_doc)
def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
customer = _make_customer(source_name, ignore_permissions)
def _make_sales_order(source_name, target_doc=None, customer_group=None, ignore_permissions=False):
customer = _make_customer(source_name, ignore_permissions, customer_group)
ordered_items = frappe._dict(
frappe.db.get_all(
"Sales Order Item",
@ -428,7 +407,7 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
return doclist
def _make_customer(source_name, ignore_permissions=False):
def _make_customer(source_name, ignore_permissions=False, customer_group=None):
quotation = frappe.db.get_value(
"Quotation", source_name, ["order_type", "party_name", "customer_name"], as_dict=1
)
@ -445,10 +424,7 @@ def _make_customer(source_name, ignore_permissions=False):
customer_doclist = _make_customer(lead_name, ignore_permissions=ignore_permissions)
customer = frappe.get_doc(customer_doclist)
customer.flags.ignore_permissions = ignore_permissions
if quotation.get("party_name") == "Shopping Cart":
customer.customer_group = frappe.db.get_value(
"E Commerce Settings", None, "default_customer_group"
)
customer.customer_group = customer_group
try:
customer.insert()

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