chore: Drive E-commerce via Website Item

- Removed Shopping Cart Settings
- Portal fully driven via E Commerce Settings
- All Item listing querying will happen via ProductQuery engine only
- Product Listing via Website Items
- removed redundant code
- Moved all website logic from Item to Website Item
This commit is contained in:
marination 2021-02-16 18:45:36 +05:30
parent 939b0dd67d
commit eef9cf152f
33 changed files with 667 additions and 1099 deletions

View File

@ -293,7 +293,7 @@ class PaymentRequest(Document):
if not status:
return
shopping_cart_settings = frappe.get_doc("Shopping Cart Settings")
shopping_cart_settings = frappe.get_doc("E Commerce Settings")
if status in ["Authorized", "Completed"]:
redirect_to = None
@ -443,7 +443,7 @@ def get_gateway_details(args):
return get_payment_gateway_account(args.get("payment_gateway_account"))
if args.order_type == "Shopping Cart":
payment_gateway_account = frappe.get_doc("Shopping Cart Settings").payment_gateway_account
payment_gateway_account = frappe.get_doc("E Commerce Settings").payment_gateway_account
return get_payment_gateway_account(payment_gateway_account)
gateway_account = get_payment_gateway_account({"is_default": 1})

View File

@ -102,7 +102,7 @@ class TaxRule(Document):
def validate_use_for_shopping_cart(self):
'''If shopping cart is enabled and no tax rule exists for shopping cart, enable this one'''
if (not self.use_for_shopping_cart
and cint(frappe.db.get_single_value('Shopping Cart Settings', 'enabled'))
and cint(frappe.db.get_single_value('E Commerce Settings', 'enabled'))
and not frappe.db.get_value('Tax Rule', {'use_for_shopping_cart': 1, 'name': ['!=', self.name]})):
self.use_for_shopping_cart = 1

View File

@ -1,13 +1,34 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('E Commerce Settings', {
frappe.ui.form.on("E Commerce Settings", {
onload: function(frm) {
if(frm.doc.__onload && frm.doc.__onload.quotation_series) {
frm.fields_dict.quotation_series.df.options = frm.doc.__onload.quotation_series;
frm.refresh_field("quotation_series");
}
frm.set_query('payment_gateway_account', function() {
return { 'filters': { 'payment_channel': "Email" } };
});
},
refresh: function(frm) {
frappe.model.with_doctype('Item', () => {
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("Item", () => {
const item_meta = frappe.get_meta('Item');
const valid_fields = item_meta.fields.filter(
df => ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden
df => ["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden
).map(df => ({ label: df.label, value: df.fieldname }));
frm.fields_dict.filter_fields.grid.update_docfield_property(
@ -17,5 +38,16 @@ frappe.ui.form.on('E Commerce Settings', {
'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', '');
}
}
});

View File

@ -10,6 +10,30 @@
"hide_variants",
"column_break_4",
"products_per_page",
"display_settings_section",
"show_attachments",
"show_price",
"show_stock_availability",
"enable_variants",
"column_break_13",
"show_contact_us_button",
"show_quantity_in_website",
"show_apply_coupon_code_in_website",
"allow_items_not_in_stock",
"section_break_18",
"company",
"price_list",
"enabled",
"store_page_docs",
"column_break_21",
"default_customer_group",
"quotation_series",
"checkout_settings_section",
"enable_checkout",
"save_quotations_as_draft",
"column_break_27",
"payment_gateway_account",
"payment_success_url",
"filter_categories_section",
"enable_field_filters",
"filter_fields",
@ -37,6 +61,7 @@
"label": "Products per Page"
},
{
"collapsible": 1,
"fieldname": "filter_categories_section",
"fieldtype": "Section Break",
"label": "Filters"
@ -76,12 +101,169 @@
"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 Variants"
},
{
"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"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-02-10 19:22:47.154104",
"modified": "2021-02-11 18:22:14.556880",
"modified_by": "Administrator",
"module": "E-commerce",
"name": "E Commerce Settings",

View File

@ -2,15 +2,18 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint
from frappe.utils import get_datetime, get_datetime_str, now_datetime
class ShoppingCartSetupError(frappe.ValidationError): pass
class ECommerceSettings(Document):
def onload(self):
self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series")
def validate(self):
if self.home_page_is_products:
frappe.db.set_value("Website Settings", None, "home_page", "products")
@ -19,6 +22,9 @@ class ECommerceSettings(Document):
self.validate_field_filters()
self.validate_attribute_filters()
if self.enabled:
self.validate_exchange_rates_exist()
frappe.clear_document_cache("E Commerce Settings", "E Commerce Settings")
def validate_field_filters(self):
@ -37,6 +43,79 @@ class ECommerceSettings(Document):
# if attribute filters are enabled, hide_variants should be disabled
self.hide_variants = 0
def validate_exchange_rates_exist(self):
"""check if exchange rates exist for all Price List currencies (to company's currency)"""
company_currency = frappe.get_cached_value('Company', self.company, "default_currency")
if not company_currency:
msgprint(_("Please specify currency in Company") + ": " + self.company,
raise_exception=ShoppingCartSetupError)
price_list_currency_map = frappe.db.get_values("Price List",
[self.price_list], "currency")
price_list_currency_map = dict(price_list_currency_map)
# check if all price lists have a currency
for price_list, currency in price_list_currency_map.items():
if not currency:
frappe.throw(_("Currency is required for Price List {0}").format(price_list))
expected_to_exist = [currency + "-" + company_currency
for currency in price_list_currency_map.values()
if currency != company_currency]
# manqala 20/09/2016: set up selection parameters for query from tabCurrency Exchange
from_currency = [currency for currency in price_list_currency_map.values() if currency != company_currency]
to_currency = company_currency
# manqala end
if expected_to_exist:
# manqala 20/09/2016: modify query so that it uses date in the selection from Currency Exchange.
# exchange rates defined with date less than the date on which this document is being saved will be selected
exists = frappe.db.sql_list("""select CONCAT(from_currency,'-',to_currency) from `tabCurrency Exchange`
where from_currency in (%s) and to_currency = "%s" and date <= curdate()""" % (", ".join(["%s"]*len(from_currency)), to_currency), tuple(from_currency))
# manqala end
missing = list(set(expected_to_exist).difference(exists))
if missing:
msgprint(_("Missing Currency Exchange Rates for {0}").format(comma_and(missing)),
raise_exception=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 validate_cart_settings(doc, method):
frappe.get_doc("E Commerce Settings", "E Commerce Settings").run_method("validate")
def get_shopping_cart_settings():
if not getattr(frappe.local, "shopping_cart_settings", None):
frappe.local.shopping_cart_settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
return frappe.local.shopping_cart_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
def home_page_is_products(doc, method):
"""Called on saving Website Settings."""

View File

@ -2,9 +2,40 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import frappe
import unittest
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ShoppingCartSetupError
class TestECommerceSettings(unittest.TestCase):
pass
def setUp(self):
frappe.db.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """)
def get_cart_settings(self):
return frappe.get_doc({"doctype": "E Commerce Settings",
"company": "_Test Company"})
def test_exchange_rate_exists(self):
frappe.db.sql("""delete from `tabCurrency Exchange`""")
cart_settings = self.get_cart_settings()
cart_settings.price_list = "_Test Price List Rest of the World"
self.assertRaises(ShoppingCartSetupError, cart_settings.validate_exchange_rates_exist)
from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \
currency_exchange_records
frappe.get_doc(currency_exchange_records[0]).insert()
cart_settings.validate_exchange_rates_exist()
def test_tax_rule_validation(self):
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
frappe.db.commit()
cart_settings = self.get_cart_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")
test_dependencies = ["Tax Rule"]

View File

@ -2,12 +2,13 @@
"actions": [],
"allow_guest_to_view": 1,
"allow_import": 1,
"autoname": "field:item_code",
"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",
@ -28,6 +29,7 @@
"thumbnail",
"section_break_17",
"website_warehouse",
"description",
"website_specifications",
"copy_from_item_group",
"column_break_27",
@ -60,8 +62,8 @@
"fieldtype": "Link",
"label": "Item Code",
"options": "Item",
"reqd": 1,
"unique": 1
"read_only_depends_on": "eval:!doc.__islocal",
"reqd": 1
},
{
"fetch_from": "item_code.item_name",
@ -246,13 +248,30 @@
{
"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
}
],
"has_web_view": 1,
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-02-10 14:22:41.628232",
"modified": "2021-02-12 16:49:42.275517",
"modified_by": "Administrator",
"module": "E-commerce",
"name": "Website Item",

View File

@ -5,13 +5,17 @@
from __future__ import unicode_literals
import frappe
import json
import itertools
from frappe import _
from six import iteritems
from frappe.website.doctype.website_slideshow.website_slideshow import \
get_slideshow
from frappe.website.render import clear_cache
from frappe.website.website_generator import WebsiteGenerator
from frappe.utils import cstr, random_string, cint
from frappe.utils import cstr, random_string
from erpnext.setup.doctype.item_group.item_group import get_parent_item_groups
class WebsiteItem(WebsiteGenerator):
website = frappe._dict(
@ -30,13 +34,23 @@ class WebsiteItem(WebsiteGenerator):
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)
def on_update(self):
self.update_template_item()
def on_trash(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
@ -48,6 +62,18 @@ class WebsiteItem(WebsiteGenerator):
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):
"""Set Show in Website for Template Item if True for its Variant"""
if self.variant_of:
if self.published:
# show template
template_item = frappe.get_doc("Item", self.variant_of)
if not template_item.published:
template_item.published = 1
template_item.flags.ignore_permissions = True
template_item.save()
def validate_website_image(self):
if frappe.flags.in_import:
return
@ -133,6 +159,164 @@ class WebsiteItem(WebsiteGenerator):
self.thumbnail = file_doc.thumbnail_url
def get_context(self, context):
print(context)
context.show_search = True
context.search_link = '/search'
context.parents = get_parent_item_groups(self.item_group)
context.body_class = "product-page"
self.attributes = frappe.get_all("Item Variant Attribute",
fields=["attribute", "attribute_value"],
filters={"parent": self.item_code})
self.set_variant_context(context)
self.set_attribute_context(context)
self.set_disabled_attributes(context)
self.set_metatags(context)
self.set_shopping_cart_data(context)
print("IN WEB ITEM")
return context
def set_variant_context(self, context):
if self.has_variants:
context.no_cache = True
# load variants
# also used in set_attribute_context
context.variants = frappe.get_all("Item",
filters={"variant_of": self.name, "show_variant_in_website": 1},
order_by="name asc")
variant = frappe.form_dict.variant
if not variant and context.variants:
# the case when the item is opened for the first time from its list
variant = context.variants[0]
if variant:
context.variant = frappe.get_doc("Item", variant)
for fieldname in ("website_image", "website_image_alt", "web_long_description", "description",
"website_specifications"):
if context.variant.get(fieldname):
value = context.variant.get(fieldname)
if isinstance(value, list):
value = [d.as_dict() for d in value]
context[fieldname] = value
def set_attribute_context(self, context):
if self.has_variants:
attribute_values_available = {}
context.attribute_values = {}
context.selected_attributes = {}
# load attributes
for v in context.variants:
v.attributes = frappe.get_all("Item Variant Attribute",
fields=["attribute", "attribute_value"],
filters={"parent": v.name})
# make a map for easier access in templates
v.attribute_map = frappe._dict({})
for attr in v.attributes:
v.attribute_map[attr.attribute] = attr.attribute_value
for attr in v.attributes:
values = attribute_values_available.setdefault(attr.attribute, [])
if attr.attribute_value not in values:
values.append(attr.attribute_value)
if v.name == context.variant.name:
context.selected_attributes[attr.attribute] = attr.attribute_value
# filter attributes, order based on attribute table
for attr in self.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)
context.variant_info = json.dumps(context.variants)
def set_disabled_attributes(self, context):
"""Disable selection options of attribute combinations that do not result in a variant"""
if not self.attributes or not self.has_variants:
return
context.disabled_attributes = {}
attributes = [attr.attribute for attr in self.attributes]
def find_variant(combination):
for variant in context.variants:
if len(variant.attributes) < len(attributes):
continue
if "combination" not in variant:
ref_combination = []
for attr in variant.attributes:
idx = attributes.index(attr.attribute)
ref_combination.insert(idx, attr.attribute_value)
variant["combination"] = ref_combination
if not (set(combination) - set(variant["combination"])):
# check if the combination is a subset of a variant combination
# eg. [Blue, 0.5] is a possible combination if exists [Blue, Large, 0.5]
return True
for i, attr in enumerate(self.attributes):
if i == 0:
continue
combination_source = []
# loop through previous attributes
for prev_attr in self.attributes[:i]:
combination_source.append([context.selected_attributes.get(prev_attr.attribute)])
combination_source.append(context.attribute_values[attr.attribute])
for combination in itertools.product(*combination_source):
if not find_variant(combination):
context.disabled_attributes.setdefault(attr.attribute, []).append(combination[-1])
def set_metatags(self, context):
context.metatags = frappe._dict({})
safe_description = frappe.utils.to_markdown(self.description)
context.metatags.url = frappe.utils.get_url() + '/' + context.route
if context.website_image:
if context.website_image.startswith('http'):
url = context.website_image
else:
url = frappe.utils.get_url() + context.website_image
context.metatags.image = url
context.metatags.description = safe_description[:300]
context.metatags.title = self.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.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 make_website_item(doc):
if not doc:
@ -141,13 +325,13 @@ def make_website_item(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"), indicator="blue")
frappe.throw(message, title=_("Already Published"))
website_item = frappe.new_doc("Website Item")
website_item.web_item_name = doc.get("item_name")
fields_to_map = ["item_code", "item_name", "item_group", "stock_uom", "brand", "image",
"has_variants", "variant_of"]
"has_variants", "variant_of", "description"]
for field in fields_to_map:
website_item.update({field: doc.get(field)})

View File

@ -240,8 +240,8 @@ doc_events = {
"erpnext.support.doctype.issue.issue.set_first_response_time"
]
},
"Sales Taxes and Charges Template": {
"on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings"
("Sales Taxes and Charges Template", "Price List"): {
"on_update": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.validate_cart_settings"
},
"Website Settings": {
"validate": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.home_page_is_products"

View File

@ -5,166 +5,6 @@ from erpnext.portal.product_configurator.item_variants_cache import ItemVariants
from erpnext.setup.doctype.item_group.item_group import get_child_groups
from erpnext.shopping_cart.product_info import get_product_info_for_website
def get_field_filter_data():
e_commerce_settings = get_e_commerce_settings()
filter_fields = [row.fieldname for row in e_commerce_settings.filter_fields]
meta = frappe.get_meta('Item')
fields = [df for df in meta.fields if df.fieldname in filter_fields]
filter_data = []
for f in fields:
doctype = f.get_link_doctype()
# apply enable/disable/show_in_website filter
meta = frappe.get_meta(doctype)
filters = {}
if meta.has_field('enabled'):
filters['enabled'] = 1
if meta.has_field('disabled'):
filters['disabled'] = 0
if meta.has_field('show_in_website'):
filters['show_in_website'] = 1
values = [d.name for d in frappe.get_all(doctype, filters)]
filter_data.append([f, values])
return filter_data
def get_attribute_filter_data():
e_commerce_settings = get_e_commerce_settings()
attributes = [row.attribute for row in e_commerce_settings.filter_attributes]
attribute_docs = [
frappe.get_doc('Item Attribute', attribute) for attribute in attributes
]
# mark attribute values as checked if they are present in the request url
if frappe.form_dict:
for attr in attribute_docs:
if attr.name in frappe.form_dict:
value = frappe.form_dict[attr.name]
if value:
enabled_values = value.split(',')
else:
enabled_values = []
for v in enabled_values:
for item_attribute_row in attr.item_attribute_values:
if v == item_attribute_row.attribute_value:
item_attribute_row.checked = True
return attribute_docs
def get_products_for_website(field_filters=None, attribute_filters=None, search=None):
if attribute_filters:
item_codes = get_item_codes_by_attributes(attribute_filters)
items_by_attributes = get_items([['name', 'in', item_codes]])
if field_filters:
items_by_fields = get_items_by_fields(field_filters)
if attribute_filters and not field_filters:
return items_by_attributes
if field_filters and not attribute_filters:
return items_by_fields
if field_filters and attribute_filters:
items_intersection = []
item_codes_in_attribute = [item.name for item in items_by_attributes]
for item in items_by_fields:
if item.name in item_codes_in_attribute:
items_intersection.append(item)
return items_intersection
if search:
return get_items(search=search)
return get_items()
@frappe.whitelist(allow_guest=True)
def get_products_html_for_website(field_filters=None, attribute_filters=None):
field_filters = frappe.parse_json(field_filters)
attribute_filters = frappe.parse_json(attribute_filters)
set_item_group_filters(field_filters)
items = get_products_for_website(field_filters, attribute_filters)
html = ''.join(get_html_for_items(items))
if not items:
html = frappe.render_template('erpnext/www/all-products/not_found.html', {})
return html
def set_item_group_filters(field_filters):
if field_filters is not None and 'item_group' in field_filters:
field_filters['item_group'] = [ig[0] for ig in get_child_groups(field_filters['item_group'])]
def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
items = []
for attribute, values in attribute_filters.items():
attribute_values = values
if not isinstance(attribute_values, list):
attribute_values = [attribute_values]
if not attribute_values: continue
wheres = []
query_values = []
for attribute_value in attribute_values:
wheres.append('( attribute = %s and attribute_value = %s )')
query_values += [attribute, attribute_value]
attribute_query = ' or '.join(wheres)
if template_item_code:
variant_of_query = 'AND t2.variant_of = %s'
query_values.append(template_item_code)
else:
variant_of_query = ''
query = '''
SELECT
t1.parent
FROM
`tabItem Variant Attribute` t1
WHERE
1 = 1
AND (
{attribute_query}
)
AND EXISTS (
SELECT
1
FROM
`tabItem` t2
WHERE
t2.name = t1.parent
{variant_of_query}
)
GROUP BY
t1.parent
ORDER BY
NULL
'''.format(attribute_query=attribute_query, variant_of_query=variant_of_query)
item_codes = set([r[0] for r in frappe.db.sql(query, query_values)])
items.append(item_codes)
res = list(set.intersection(*items))
return res
@frappe.whitelist(allow_guest=True)
def get_attributes_and_values(item_code):
'''Build a list of attributes and their possible values.
@ -278,140 +118,6 @@ def get_items_with_selected_attributes(item_code, selected_attributes):
return set.intersection(*items)
def get_items_by_fields(field_filters):
meta = frappe.get_meta('Item')
filters = []
for fieldname, values in field_filters.items():
if not values: continue
_doctype = 'Item'
_fieldname = fieldname
df = meta.get_field(fieldname)
if df.fieldtype == 'Table MultiSelect':
child_doctype = df.options
child_meta = frappe.get_meta(child_doctype)
fields = child_meta.get("fields", { "fieldtype": "Link", "in_list_view": 1 })
if fields:
_doctype = child_doctype
_fieldname = fields[0].fieldname
if len(values) == 1:
filters.append([_doctype, _fieldname, '=', values[0]])
else:
filters.append([_doctype, _fieldname, 'in', values])
return get_items(filters)
def get_items(filters=None, search=None):
start = frappe.form_dict.start or 0
e_commerce_settings = get_e_commerce_settings()
page_length = e_commerce_settings.products_per_page
filters = filters or []
# convert to list of filters
if isinstance(filters, dict):
filters = [['Item', fieldname, '=', value] for fieldname, value in filters.items()]
enabled_items_filter = get_conditions({ 'disabled': 0 }, 'and')
show_in_website_condition = ''
if e_commerce_settings.hide_variants:
show_in_website_condition = get_conditions({'show_in_website': 1 }, 'and')
else:
show_in_website_condition = get_conditions([
['show_in_website', '=', 1],
['show_variant_in_website', '=', 1]
], 'or')
search_condition = ''
if search:
# Default fields to search from
default_fields = {'name', 'item_name', 'description', 'item_group'}
# Get meta search fields
meta = frappe.get_meta("Item")
meta_fields = set(meta.get_search_fields())
# Join the meta fields and default fields set
search_fields = default_fields.union(meta_fields)
try:
if frappe.db.count('Item', cache=True) > 50000:
search_fields.remove('description')
except KeyError:
pass
# Build or filters for query
search = '%{}%'.format(search)
or_filters = [[field, 'like', search] for field in search_fields]
search_condition = get_conditions(or_filters, 'or')
filter_condition = get_conditions(filters, 'and')
where_conditions = ' and '.join(
[condition for condition in [enabled_items_filter, show_in_website_condition, \
search_condition, filter_condition] if condition]
)
left_joins = []
for f in filters:
if len(f) == 4 and f[0] != 'Item':
left_joins.append(f[0])
left_join = ' '.join(['LEFT JOIN `tab{0}` on (`tab{0}`.parent = `tabItem`.name)'.format(l) for l in left_joins])
results = frappe.db.sql('''
SELECT
`tabItem`.`name`, `tabItem`.`item_name`, `tabItem`.`item_code`,
`tabItem`.`website_image`, `tabItem`.`image`,
`tabItem`.`web_long_description`, `tabItem`.`description`,
`tabItem`.`route`, `tabItem`.`item_group`
FROM
`tabItem`
{left_join}
WHERE
{where_conditions}
GROUP BY
`tabItem`.`name`
ORDER BY
`tabItem`.`weightage` DESC
LIMIT
{page_length}
OFFSET
{start}
'''.format(
where_conditions=where_conditions,
start=start,
page_length=page_length,
left_join=left_join
)
, as_dict=1)
for r in results:
r.description = r.web_long_description or r.description
r.image = r.website_image or r.image
product_info = get_product_info_for_website(r.item_code, skip_quotation_creation=True).get('product_info')
if product_info:
r.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None
return results
def get_conditions(filter_list, and_or='and'):
from frappe.model.db_query import DatabaseQuery
if not filter_list:
return ''
conditions = []
DatabaseQuery('Item').build_filter_conditions(filter_list, conditions, ignore_permissions=True)
join_by = ' {0} '.format(and_or)
return '(' + join_by.join(conditions) + ')'
# utilities
def get_item_attributes(item_code):

View File

@ -1,10 +1,8 @@
from __future__ import unicode_literals
import frappe
from frappe.utils.nestedset import get_root_of
from erpnext.shopping_cart.cart import get_debtors_account
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
get_shopping_cart_settings,
)

View File

@ -180,7 +180,7 @@ $.extend(shopping_cart, {
show_cart_navbar: function () {
frappe.call({
method: "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.is_cart_enabled",
method: "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.is_cart_enabled",
callback: function(r) {
$(".shopping-cart").toggleClass('hidden', r.message ? false : true);
}

View File

@ -274,7 +274,7 @@ def _make_customer(source_name, ignore_permissions=False):
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("Shopping Cart Settings", None,
customer.customer_group = frappe.db.get_value("E Commerce Settings", None,
"default_customer_group")
try:

View File

@ -120,9 +120,8 @@ class ItemGroup(NestedSet, WebsiteGenerator):
values[f"slide_{index + 1}_image"] = slide.image
values[f"slide_{index + 1}_title"] = slide.heading
values[f"slide_{index + 1}_subtitle"] = slide.description
values[f"slide_{index + 1}_theme"] = slide.theme or "Light"
values[f"slide_{index + 1}_content_align"] = slide.content_align or "Centre"
values[f"slide_{index + 1}_primary_action_label"] = slide.label
values[f"slide_{index + 1}_theme"] = slide.get("theme") or "Light"
values[f"slide_{index + 1}_content_align"] = slide.get("content_align") or "Centre"
values[f"slide_{index + 1}_primary_action"] = slide.url
context.slideshow = values
@ -175,7 +174,7 @@ def get_product_list_for_group(product_group=None, start=0, limit=10, search=Non
data = frappe.db.sql(query, {"product_group": product_group,"search": search, "today": nowdate()}, as_dict=1)
data = adjust_qty_for_expired_items(data)
if cint(frappe.db.get_single_value("Shopping Cart Settings", "enabled")):
if cint(frappe.db.get_single_value("E Commerce Settings", "enabled")):
for item in data:
set_product_info_for_website(item)

View File

@ -33,7 +33,7 @@ def create_fiscal_year_and_company(args):
def enable_shopping_cart(args):
# Needs price_lists
frappe.get_doc({
"doctype": "Shopping Cart Settings",
"doctype": "E Commerce Settings",
"enabled": 1,
'company': args.get('company_name') ,
'price_list': frappe.db.get_value("Price List", {"selling": 1}),

View File

@ -534,7 +534,7 @@ def create_bank_account(args):
pass
def update_shopping_cart_settings(args):
shopping_cart = frappe.get_doc("Shopping Cart Settings")
shopping_cart = frappe.get_doc("E Commerce Settings")
shopping_cart.update({
"enabled": 1,
'company': args.company_name,

View File

@ -1,8 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
import frappe.defaults
from frappe import _, throw
@ -11,10 +9,8 @@ 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.e_commerce.doctype.e_commerce_settings.e_commerce_settings import get_shopping_cart_settings
from erpnext.accounts.utils import get_account_name
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
get_shopping_cart_settings,
)
from erpnext.utilities.product import get_qty_in_stock
@ -22,7 +18,7 @@ class WebsitePriceListMissingError(frappe.ValidationError):
pass
def set_cart_count(quotation=None):
if cint(frappe.db.get_singles_value("Shopping Cart Settings", "enabled")):
if cint(frappe.db.get_singles_value("E Commerce Settings", "enabled")):
if not quotation:
quotation = _get_cart_quotation()
cart_count = cstr(len(quotation.get("items")))
@ -49,7 +45,7 @@ def get_cart_quotation(doc=None):
"shipping_addresses": get_shipping_addresses(party),
"billing_addresses": get_billing_addresses(party),
"shipping_rules": get_applicable_shipping_rules(party),
"cart_settings": frappe.get_cached_doc("Shopping Cart Settings")
"cart_settings": frappe.get_cached_doc("E Commerce Settings")
}
@frappe.whitelist()
@ -73,7 +69,7 @@ def get_billing_addresses(party=None):
@frappe.whitelist()
def place_order():
quotation = _get_cart_quotation()
cart_settings = frappe.db.get_value("Shopping Cart Settings", None,
cart_settings = frappe.db.get_value("E Commerce Settings", None,
["company", "allow_items_not_in_stock"], as_dict=1)
quotation.company = cart_settings.company
@ -263,7 +259,7 @@ def guess_territory():
territory = frappe.db.get_value("Territory", geoip_country)
return territory or \
frappe.db.get_value("Shopping Cart Settings", None, "territory") or \
frappe.db.get_value("E Commerce Settings", None, "territory") or \
get_root_of("Territory")
def decorate_quotation_doc(doc):
@ -286,7 +282,7 @@ def _get_cart_quotation(party=None):
if quotation:
qdoc = frappe.get_doc("Quotation", quotation[0].name)
else:
company = frappe.db.get_value("Shopping Cart Settings", None, ["company"])
company = frappe.db.get_value("E Commerce Settings", None, ["company"])
qdoc = frappe.get_doc({
"doctype": "Quotation",
"naming_series": get_shopping_cart_settings().quotation_series or "QTN-CART-",
@ -341,7 +337,7 @@ def apply_cart_settings(party=None, quotation=None):
if not quotation:
quotation = _get_cart_quotation(party)
cart_settings = frappe.get_doc("Shopping Cart Settings")
cart_settings = frappe.get_doc("E Commerce Settings")
set_price_list_and_rate(quotation, cart_settings)
@ -418,7 +414,7 @@ def get_party(user=None):
party_doctype = contact.links[0].link_doctype
party = contact.links[0].link_name
cart_settings = frappe.get_doc("Shopping Cart Settings")
cart_settings = frappe.get_doc("E Commerce Settings")
debtors_account = ''

View File

@ -1,38 +0,0 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Shopping Cart 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>`
);
}
},
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', '');
}
}
});

View File

@ -1,212 +0,0 @@
{
"actions": [],
"creation": "2013-06-19 15:57:32",
"description": "Default settings for Shopping Cart",
"doctype": "DocType",
"document_type": "System",
"engine": "InnoDB",
"field_order": [
"enabled",
"store_page_docs",
"display_settings",
"show_attachments",
"show_price",
"show_stock_availability",
"enable_variants",
"column_break_7",
"show_contact_us_button",
"show_quantity_in_website",
"show_apply_coupon_code_in_website",
"allow_items_not_in_stock",
"section_break_2",
"company",
"price_list",
"column_break_4",
"default_customer_group",
"quotation_series",
"section_break_8",
"enable_checkout",
"save_quotations_as_draft",
"column_break_11",
"payment_gateway_account",
"payment_success_url"
],
"fields": [
{
"default": "0",
"fieldname": "enabled",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Enable Shopping Cart"
},
{
"fieldname": "display_settings",
"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": "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"
},
{
"depends_on": "enabled",
"fieldname": "section_break_2",
"fieldtype": "Section Break"
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"mandatory_depends_on": "eval: doc.enabled === 1",
"options": "Company",
"remember_last_selected_value": 1
},
{
"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_4",
"fieldtype": "Column Break"
},
{
"fieldname": "default_customer_group",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Default Customer Group",
"mandatory_depends_on": "eval: doc.enabled === 1",
"options": "Customer Group"
},
{
"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": "section_break_8",
"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_11",
"fieldtype": "Column Break"
},
{
"depends_on": "enable_checkout",
"fieldname": "payment_gateway_account",
"fieldtype": "Link",
"label": "Payment Gateway Account",
"mandatory_depends_on": "enable_checkout",
"options": "Payment Gateway Account"
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "enable_variants",
"fieldtype": "Check",
"label": "Enable Variants"
},
{
"default": "0",
"depends_on": "eval: doc.enable_checkout == 0",
"fieldname": "save_quotations_as_draft",
"fieldtype": "Check",
"label": "Save Quotations as Draft"
},
{
"depends_on": "doc.enabled",
"fieldname": "store_page_docs",
"fieldtype": "HTML"
}
],
"icon": "fa fa-shopping-cart",
"idx": 1,
"issingle": 1,
"links": [],
"modified": "2021-03-02 17:34:57.642565",
"modified_by": "Administrator",
"module": "Shopping Cart",
"name": "Shopping Cart Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Website Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}

View File

@ -1,85 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import flt
class ShoppingCartSetupError(frappe.ValidationError): pass
class ShoppingCartSettings(Document):
def onload(self):
self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series")
def validate(self):
if self.enabled:
self.validate_price_list_exchange_rate()
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 validate_cart_settings(doc=None, method=None):
frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings").run_method("validate")
def get_shopping_cart_settings():
if not getattr(frappe.local, "shopping_cart_settings", None):
frappe.local.shopping_cart_settings = frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings")
return frappe.local.shopping_cart_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,56 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
# For license information, please see license.txt
from __future__ import unicode_literals
import unittest
import frappe
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
ShoppingCartSetupError,
)
class TestShoppingCartSettings(unittest.TestCase):
def setUp(self):
frappe.db.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """)
def get_cart_settings(self):
return frappe.get_doc({"doctype": "Shopping Cart Settings",
"company": "_Test Company"})
# NOTE: Exchangrate API has all enabled currencies that ERPNext supports.
# We aren't checking just currency exchange record anymore
# while validating price list currency exchange rate to that of company.
# The API is being used to fetch the rate which again almost always
# gives back a valid value (for valid currencies).
# This makes the test obsolete.
# Commenting because im not sure if there's a better test we can write
# def test_exchange_rate_exists(self):
# frappe.db.sql("""delete from `tabCurrency Exchange`""")
# cart_settings = self.get_cart_settings()
# cart_settings.price_list = "_Test Price List Rest of the World"
# self.assertRaises(ShoppingCartSetupError, cart_settings.validate_price_list_exchange_rate)
# from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \
# currency_exchange_records
# frappe.get_doc(currency_exchange_records[0]).insert()
# cart_settings.validate_price_list_exchange_rate()
def test_tax_rule_validation(self):
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
frappe.db.commit()
cart_settings = self.get_cart_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")
test_dependencies = ["Tax Rule"]

View File

@ -1,17 +1,14 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from erpnext.shopping_cart.cart import _get_cart_quotation, _set_price_list
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
get_shopping_cart_settings,
show_quantity_in_website,
show_quantity_in_website
)
from erpnext.utilities.product import get_non_stock_item_status, get_price, get_qty_in_stock
from erpnext.utilities.product import get_price, get_qty_in_stock, get_non_stock_item_status
@frappe.whitelist(allow_guest=True)
def get_product_info_for_website(item_code, skip_quotation_creation=False):

View File

@ -10,26 +10,22 @@ class ProductQuery:
"""Query engine for product listing
Attributes:
cart_settings (Document): Settings for Cart
fields (list): Fields to fetch in query
filters (TYPE): Description
or_filters (list): Description
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
filters (list)
or_filters (list)
"""
def __init__(self):
self.settings = frappe.get_doc("E Commerce Settings")
self.cart_settings = frappe.get_doc("Shopping Cart Settings")
self.page_length = self.settings.products_per_page or 20
self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants',
'item_group', 'image', 'web_long_description', 'description', 'route', 'weightage']
self.filters = []
self.or_filters = [['show_in_website', '=', 1]]
if not self.settings.get('hide_variants'):
self.or_filters.append(['show_variant_in_website', '=', 1])
self.fields = ['wi.name', 'wi.item_name', 'wi.item_code', 'wi.website_image', 'wi.variant_of',
'wi.has_variants', 'wi.item_group', 'wi.image', 'wi.web_long_description', 'wi.description',
'wi.route']
self.conditions = ""
self.or_conditions = ""
self.substitutions = []
def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None):
"""Summary
@ -57,51 +53,14 @@ class ProductQuery:
filters=[["Website Item Group", "item_group", "=", item_group]]
)
self.query_fields = (", ").join(self.fields)
if attributes:
all_items = []
for attribute, values in attributes.items():
if not isinstance(values, list):
values = [values]
items = frappe.get_all(
"Item",
fields=self.fields,
filters=[
*self.filters,
["Item Variant Attribute", "attribute", "=", attribute],
["Item Variant Attribute", "attribute_value", "in", values],
],
or_filters=self.or_filters,
start=start,
limit=self.page_length,
order_by="weightage desc"
)
items_dict = {item.name: item for item in items}
all_items.append(set(items_dict.keys()))
result = [items_dict.get(item) for item in list(set.intersection(*all_items))]
result = self.query_items_with_attributes(attributes, start)
else:
result = frappe.get_all(
"Item",
fields=self.fields,
filters=self.filters,
or_filters=self.or_filters,
start=start,
limit=self.page_length,
order_by="weightage desc"
)
# Combine results having context of website item groups into item results
if item_group and website_item_groups:
items_list = {row.name for row in result}
for row in website_item_groups:
if row.wig_parent not in items_list:
result.append(row)
result = sorted(result, key=lambda x: x.get("weightage"), reverse=True)
result = self.query_items(self.conditions, self.or_conditions,
self.substitutions, start=start)
# add price info in results
for item in result:
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
if product_info:
@ -109,6 +68,51 @@ class ProductQuery:
return result
def query_items(self, conditions, or_conditions, substitutions, start=0):
"""Build a query to fetch Website Items based on field filters."""
return frappe.db.sql("""
select distinct {query_fields}
from
`tabWebsite Item` wi, `tabItem Variant Attribute` iva
where
wi.published = 1
{conditions}
{or_conditions}
limit {limit} offset {start}
""".format(
query_fields=self.query_fields,
conditions=conditions,
or_conditions=or_conditions,
limit=self.page_length,
start=start),
tuple(substitutions),
as_dict=1)
def query_items_with_attributes(self, attributes, start=0):
"""Build a query to fetch Website Items based on field & attribute filters."""
all_items = []
self.conditions += " and iva.parent = wi.item_code"
for attribute, values in attributes.items():
if not isinstance(values, list): values = [values]
conditions_copy = self.conditions
substitutions_copy = self.substitutions.copy()
conditions_copy += " and iva.attribute = '{0}' and iva.attribute_value in ({1})" \
.format(attribute, (", ").join(['%s'] * len(values)))
substitutions_copy.extend(values)
items = self.query_items(conditions_copy, self.or_conditions, substitutions_copy, start=start)
items_dict = {item.name: item for item in items}
# TODO: Replace Variants by their parent templates
all_items.append(set(items_dict.keys()))
result = [items_dict.get(item) for item in list(set.intersection(*all_items))]
return result
def build_fields_filters(self, filters):
"""Build filters for field values
@ -130,10 +134,11 @@ class ProductQuery:
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])
self.conditions += " and wi.{0} in ({1})".format(field, (', ').join(['%s'] * len(values)))
self.substitutions.extend(values)
else:
# `=` will be faster than `IN` for most cases
self.filters.append([field, '=', values])
self.conditions += " and wi.{0} = '{1}'".format(field, values)
def build_search_filters(self, search_term):
"""Query search term in specified fields
@ -158,4 +163,5 @@ class ProductQuery:
# Build or filters for query
search = '%{}%'.format(search_term)
self.or_filters += [[field, 'like', search] for field in search_fields]
for field in search_fields:
self.or_conditions += " or {0} like '{1}'".format(field, search)

View File

@ -167,7 +167,7 @@ class TestShoppingCart(unittest.TestCase):
# helper functions
def enable_shopping_cart(self):
settings = frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings")
settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
settings.update({
"enabled": 1,
@ -197,7 +197,7 @@ class TestShoppingCart(unittest.TestCase):
frappe.local.shopping_cart_settings = None
def disable_shopping_cart(self):
settings = frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings")
settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
settings.enabled = 0
settings.save()
frappe.local.shopping_cart_settings = None

View File

@ -1,14 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
is_cart_enabled,
)
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import is_cart_enabled
def show_cart_count():
if (is_cart_enabled() and

View File

@ -21,7 +21,6 @@ from frappe.utils import (
strip,
)
from frappe.utils.html_utils import clean_html
from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow
from frappe.website.utils import clear_cache
from frappe.website.website_generator import WebsiteGenerator
@ -131,8 +130,6 @@ class Item(WebsiteGenerator):
self.validate_attributes()
self.validate_variant_attributes()
self.validate_variant_based_on_change()
self.validate_website_image()
self.make_thumbnail()
self.validate_fixed_asset()
self.validate_retain_sample()
self.validate_uom_conversion_factor()
@ -141,7 +138,6 @@ class Item(WebsiteGenerator):
self.validate_item_defaults()
self.validate_auto_reorder_enabled_in_stock_settings()
self.cant_change()
self.update_show_in_website()
self.validate_item_tax_net_rate_range()
set_item_tax_from_hsn_code(self)
@ -156,7 +152,6 @@ class Item(WebsiteGenerator):
self.validate_name_with_item_group()
self.update_variants()
self.update_item_price()
self.update_template_item()
def validate_description(self):
'''Clean HTML description if set'''
@ -218,95 +213,6 @@ class Item(WebsiteGenerator):
stock_entry.add_comment("Comment", _("Opening Stock"))
def make_route(self):
if not self.route:
return cstr(frappe.db.get_value('Item Group', self.item_group,
'route')) + '/' + self.scrub((self.item_name or self.item_code) + '-' + random_string(5))
def validate_website_image(self):
if frappe.flags.in_import:
return
"""Validate if the website image is a public file"""
auto_set_website_image = False
if not self.website_image and self.image:
auto_set_website_image = True
self.website_image = self.image
if not self.website_image:
return
# find if website image url exists as public
file_doc = frappe.get_all("File", filters={
"file_url": self.website_image
}, fields=["name", "is_private"], order_by="is_private asc", limit_page_length=1)
if file_doc:
file_doc = file_doc[0]
if not file_doc:
if not auto_set_website_image:
frappe.msgprint(_("Website Image {0} attached to Item {1} cannot be found").format(self.website_image, self.name))
self.website_image = None
elif file_doc.is_private:
if not auto_set_website_image:
frappe.msgprint(_("Website Image should be a public file or website URL"))
self.website_image = None
def make_thumbnail(self):
if frappe.flags.in_import:
return
"""Make a thumbnail of `website_image`"""
import requests.exceptions
if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"):
self.thumbnail = None
if self.website_image and not self.thumbnail:
file_doc = None
try:
file_doc = frappe.get_doc("File", {
"file_url": self.website_image,
"attached_to_doctype": "Item",
"attached_to_name": self.name
})
except frappe.DoesNotExistError:
# 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": "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 validate_fixed_asset(self):
if self.is_fixed_asset:
if self.is_stock_item:
@ -330,167 +236,6 @@ class Item(WebsiteGenerator):
frappe.throw(_("{0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format(
self.item_code))
def get_context(self, context):
context.show_search = True
context.search_link = '/product_search'
context.parents = get_parent_item_groups(self.item_group)
context.body_class = "product-page"
self.set_variant_context(context)
self.set_attribute_context(context)
self.set_disabled_attributes(context)
self.set_metatags(context)
self.set_shopping_cart_data(context)
return context
def set_variant_context(self, context):
if self.has_variants:
context.no_cache = True
# load variants
# also used in set_attribute_context
context.variants = frappe.get_all("Item",
filters={"variant_of": self.name, "show_variant_in_website": 1},
order_by="name asc")
variant = frappe.form_dict.variant
if not variant and context.variants:
# the case when the item is opened for the first time from its list
variant = context.variants[0]
if variant:
context.variant = frappe.get_doc("Item", variant)
for fieldname in ("website_image", "website_image_alt", "web_long_description", "description",
"website_specifications"):
if context.variant.get(fieldname):
value = context.variant.get(fieldname)
if isinstance(value, list):
value = [d.as_dict() for d in value]
context[fieldname] = value
if self.slideshow:
if context.variant and context.variant.slideshow:
context.update(get_slideshow(context.variant))
else:
context.update(get_slideshow(self))
def set_attribute_context(self, context):
if not self.has_variants:
return
attribute_values_available = {}
context.attribute_values = {}
context.selected_attributes = {}
# load attributes
for v in context.variants:
v.attributes = frappe.get_all("Item Variant Attribute",
fields=["attribute", "attribute_value"],
filters={"parent": v.name})
# make a map for easier access in templates
v.attribute_map = frappe._dict({})
for attr in v.attributes:
v.attribute_map[attr.attribute] = attr.attribute_value
for attr in v.attributes:
values = attribute_values_available.setdefault(attr.attribute, [])
if attr.attribute_value not in values:
values.append(attr.attribute_value)
if v.name == context.variant.name:
context.selected_attributes[attr.attribute] = attr.attribute_value
# filter attributes, order based on attribute table
for attr in self.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)
context.variant_info = json.dumps(context.variants)
def set_disabled_attributes(self, context):
"""Disable selection options of attribute combinations that do not result in a variant"""
if not self.attributes or not self.has_variants:
return
context.disabled_attributes = {}
attributes = [attr.attribute for attr in self.attributes]
def find_variant(combination):
for variant in context.variants:
if len(variant.attributes) < len(attributes):
continue
if "combination" not in variant:
ref_combination = []
for attr in variant.attributes:
idx = attributes.index(attr.attribute)
ref_combination.insert(idx, attr.attribute_value)
variant["combination"] = ref_combination
if not (set(combination) - set(variant["combination"])):
# check if the combination is a subset of a variant combination
# eg. [Blue, 0.5] is a possible combination if exists [Blue, Large, 0.5]
return True
for i, attr in enumerate(self.attributes):
if i == 0:
continue
combination_source = []
# loop through previous attributes
for prev_attr in self.attributes[:i]:
combination_source.append([context.selected_attributes.get(prev_attr.attribute)])
combination_source.append(context.attribute_values[attr.attribute])
for combination in itertools.product(*combination_source):
if not find_variant(combination):
context.disabled_attributes.setdefault(attr.attribute, []).append(combination[-1])
def set_metatags(self, context):
context.metatags = frappe._dict({})
safe_description = frappe.utils.to_markdown(self.description)
context.metatags.url = frappe.utils.get_url() + '/' + context.route
if context.website_image:
if context.website_image.startswith('http'):
url = context.website_image
else:
url = frappe.utils.get_url() + context.website_image
context.metatags.image = url
context.metatags.description = safe_description[:300]
context.metatags.title = self.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.shopping_cart.product_info import get_product_info_for_website
context.shopping_cart = get_product_info_for_website(self.name, skip_quotation_creation=True)
def add_default_uom_in_conversion_factor_table(self):
uom_conv_list = [d.uom for d in self.get("uoms")]
if self.stock_uom not in uom_conv_list:
@ -505,10 +250,6 @@ class Item(WebsiteGenerator):
[self.remove(d) for d in to_remove]
def update_show_in_website(self):
if self.disabled:
self.show_in_website = False
def validate_item_tax_net_rate_range(self):
for tax in self.get('taxes'):
if flt(tax.maximum_net_rate) < flt(tax.minimum_net_rate):
@ -678,7 +419,7 @@ class Item(WebsiteGenerator):
if merge:
self.validate_duplicate_item_in_stock_reconciliation(old_name, new_name)
if self.route:
if self.published_in_website:
invalidate_cache_for_item(self)
clear_cache(self.route)
@ -777,25 +518,6 @@ class Item(WebsiteGenerator):
where item_code = %s and docstatus < 2
""", (self.description, self.name))
def update_template_item(self):
"""Set Show in Website for Template Item if True for its Variant"""
if not self.variant_of:
return
if self.show_in_website:
self.show_variant_in_website = 1
self.show_in_website = 0
if self.show_variant_in_website:
# show template
template_item = frappe.get_doc("Item", self.variant_of)
if not template_item.show_in_website:
template_item.show_in_website = 1
template_item.flags.dont_update_variants = True
template_item.flags.ignore_permissions = True
template_item.save()
def validate_item_defaults(self):
companies = {row.company for row in self.item_defaults}
@ -1065,7 +787,6 @@ class Item(WebsiteGenerator):
'item_code': item,
'item_name': item,
'description': item,
'show_in_website': 1,
'is_sales_item': 1,
'is_purchase_item': 1,
'is_stock_item': 1,

View File

@ -23,7 +23,7 @@
})
</script>
{% else %}
{{ product_image(website_image or image or 'no-image.jpg', alt=website_image_alt or item_name) }}
{{ product_image(doc.website_image or doc.image or 'no-image.jpg', alt=doc.website_image_alt or doc.item_name) }}
{% endif %}
<!-- Simple image preview -->

View File

@ -4,7 +4,7 @@
{% set select_address = True %}
{% endif %}
{% set show_coupon_code = frappe.db.get_single_value('Shopping Cart Settings', 'show_apply_coupon_code_in_website') %}
{% set show_coupon_code = frappe.db.get_single_value('E Commerce Settings', 'show_apply_coupon_code_in_website') %}
{% if show_coupon_code == 1%}
<div class="mb-3">
<div class="row no-gutters">

View File

@ -1,4 +1,4 @@
{% from "erpnext/templates/includes/macros.html" import item_card, item_card_body %}
{% from "erpnext/templates/includes/macros.html" import item_card, item_card_body, product_image_square %}
<a class="product-link product-list-link" href="{{ route|abs_url }}">
<div class='row'>

View File

@ -6,10 +6,7 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
show_attachments,
)
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import show_attachments
def get_context(context):
context.no_cache = 1
@ -26,7 +23,7 @@ def get_context(context):
context.payment_ref = frappe.db.get_value("Payment Request",
{"reference_name": frappe.form_dict.name}, "name")
context.enabled_checkout = frappe.get_doc("Shopping Cart Settings").enable_checkout
context.enabled_checkout = frappe.get_doc("E Commerce Settings").enable_checkout
default_print_format = frappe.db.get_value('Property Setter', dict(property='default_print_format', doc_type=frappe.form_dict.doctype), "value")
if default_print_format:

View File

@ -129,7 +129,7 @@ $(() => {
Object.assign(field_filters, { item_group });
}
return new Promise((resolve, reject) => {
frappe.call('erpnext.portal.product_configurator.utils.get_products_html_for_website', args)
frappe.call('erpnext.www.all-products.index.get_products_html_for_website', args)
.then(r => {
if (r.exc) reject(r.exc);
else resolve(r.message);

View File

@ -1,9 +1,7 @@
import frappe
from erpnext.portal.product_configurator.utils import (get_products_for_website, get_e_commerce_settings,
get_field_filter_data, get_attribute_filter_data)
from erpnext.shopping_cart.filters import ProductFiltersBuilder
from frappe.utils import cint
from erpnext.shopping_cart.product_query import ProductQuery
from erpnext.shopping_cart.filters import ProductFiltersBuilder
sitemap = 1
@ -13,7 +11,7 @@ def get_context(context):
search = frappe.form_dict.search
field_filters = frappe.parse_json(frappe.form_dict.field_filters)
attribute_filters = frappe.parse_json(frappe.form_dict.attribute_filters)
start = frappe.parse_json(frappe.form_dict.start)
start = cint(frappe.parse_json(frappe.form_dict.start))
else:
search = field_filters = attribute_filters = None
start = 0
@ -24,15 +22,34 @@ def get_context(context):
# Add homepage as parent
context.parents = [{"name": frappe._("Home"), "route":"/"}]
e_commerce_settings = get_e_commerce_settings()
filter_engine = ProductFiltersBuilder()
context.field_filters = filter_engine.get_field_filters()
context.attribute_filters = filter_engine.get_attribute_filters()
context.e_commerce_settings = e_commerce_settings
context.e_commerce_settings = engine.settings
context.body_class = "product-page"
context.page_length = e_commerce_settings.products_per_page or 20
context.page_length = engine.settings.products_per_page or 20
context.no_cache = 1
print(context)
@frappe.whitelist(allow_guest=True)
def get_products_html_for_website(field_filters=None, attribute_filters=None):
"""Get Products on filter change."""
field_filters = frappe.parse_json(field_filters)
attribute_filters = frappe.parse_json(attribute_filters)
engine = ProductQuery()
items = engine.query(attribute_filters, field_filters, search_term=None, start=0)
item_html = []
for item in items:
item_html.append(frappe.render_template('erpnext/www/all-products/item_row.html', {
'item': item
}))
html = ''.join(item_html)
if not items:
html = frappe.render_template('erpnext/www/all-products/not_found.html', {})
return html