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

View File

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

View File

@ -1,13 +1,34 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // 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) { 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 item_meta = frappe.get_meta('Item');
const valid_fields = item_meta.fields.filter( 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 })); ).map(df => ({ label: df.label, value: df.fieldname }));
frm.fields_dict.filter_fields.grid.update_docfield_property( frm.fields_dict.filter_fields.grid.update_docfield_property(
@ -17,5 +38,16 @@ frappe.ui.form.on('E Commerce Settings', {
'fieldname', 'options', valid_fields '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", "hide_variants",
"column_break_4", "column_break_4",
"products_per_page", "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", "filter_categories_section",
"enable_field_filters", "enable_field_filters",
"filter_fields", "filter_fields",
@ -37,6 +61,7 @@
"label": "Products per Page" "label": "Products per Page"
}, },
{ {
"collapsible": 1,
"fieldname": "filter_categories_section", "fieldname": "filter_categories_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Filters" "label": "Filters"
@ -76,12 +101,169 @@
"fieldtype": "Table", "fieldtype": "Table",
"label": "Attributes", "label": "Attributes",
"options": "Website Attribute" "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, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-02-10 19:22:47.154104", "modified": "2021-02-11 18:22:14.556880",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "E-commerce", "module": "E-commerce",
"name": "E Commerce Settings", "name": "E Commerce Settings",

View File

@ -2,15 +2,18 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint from frappe.utils import cint
from frappe.utils import get_datetime, get_datetime_str, now_datetime
class ShoppingCartSetupError(frappe.ValidationError): pass
class ECommerceSettings(Document): class ECommerceSettings(Document):
def onload(self):
self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series")
def validate(self): def validate(self):
if self.home_page_is_products: if self.home_page_is_products:
frappe.db.set_value("Website Settings", None, "home_page", "products") frappe.db.set_value("Website Settings", None, "home_page", "products")
@ -19,6 +22,9 @@ class ECommerceSettings(Document):
self.validate_field_filters() self.validate_field_filters()
self.validate_attribute_filters() self.validate_attribute_filters()
if self.enabled:
self.validate_exchange_rates_exist()
frappe.clear_document_cache("E Commerce Settings", "E Commerce Settings") frappe.clear_document_cache("E Commerce Settings", "E Commerce Settings")
def validate_field_filters(self): def validate_field_filters(self):
@ -37,6 +43,79 @@ class ECommerceSettings(Document):
# if attribute filters are enabled, hide_variants should be disabled # if attribute filters are enabled, hide_variants should be disabled
self.hide_variants = 0 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): def home_page_is_products(doc, method):
"""Called on saving Website Settings.""" """Called on saving Website Settings."""

View File

@ -2,9 +2,40 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe
# import frappe
import unittest import unittest
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ShoppingCartSetupError
class TestECommerceSettings(unittest.TestCase): 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": [], "actions": [],
"allow_guest_to_view": 1, "allow_guest_to_view": 1,
"allow_import": 1, "allow_import": 1,
"autoname": "field:item_code", "autoname": "naming_series",
"creation": "2021-02-09 21:06:14.441698", "creation": "2021-02-09 21:06:14.441698",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"naming_series",
"web_item_name", "web_item_name",
"route", "route",
"has_variants", "has_variants",
@ -28,6 +29,7 @@
"thumbnail", "thumbnail",
"section_break_17", "section_break_17",
"website_warehouse", "website_warehouse",
"description",
"website_specifications", "website_specifications",
"copy_from_item_group", "copy_from_item_group",
"column_break_27", "column_break_27",
@ -60,8 +62,8 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Item Code", "label": "Item Code",
"options": "Item", "options": "Item",
"reqd": 1, "read_only_depends_on": "eval:!doc.__islocal",
"unique": 1 "reqd": 1
}, },
{ {
"fetch_from": "item_code.item_name", "fetch_from": "item_code.item_name",
@ -246,13 +248,30 @@
{ {
"fieldname": "column_break_22", "fieldname": "column_break_22",
"fieldtype": "Column Break" "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, "has_web_view": 1,
"image_field": "image", "image_field": "image",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-02-10 14:22:41.628232", "modified": "2021-02-12 16:49:42.275517",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "E-commerce", "module": "E-commerce",
"name": "Website Item", "name": "Website Item",

View File

@ -5,13 +5,17 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import json import json
import itertools
from frappe import _
from six import iteritems
from frappe.website.doctype.website_slideshow.website_slideshow import \ from frappe.website.doctype.website_slideshow.website_slideshow import \
get_slideshow get_slideshow
from frappe.website.render import clear_cache from frappe.website.render import clear_cache
from frappe.website.website_generator import WebsiteGenerator 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): class WebsiteItem(WebsiteGenerator):
website = frappe._dict( website = frappe._dict(
@ -30,13 +34,23 @@ class WebsiteItem(WebsiteGenerator):
if not self.item_code: if not self.item_code:
frappe.throw(_("Item Code is required"), title=_("Mandatory")) frappe.throw(_("Item Code is required"), title=_("Mandatory"))
self.validate_duplicate_website_item()
self.validate_website_image() self.validate_website_image()
self.make_thumbnail() self.make_thumbnail()
self.publish_unpublish_desk_item(publish=True) self.publish_unpublish_desk_item(publish=True)
def on_update(self):
self.update_template_item()
def on_trash(self): def on_trash(self):
self.publish_unpublish_desk_item(publish=False) 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): def publish_unpublish_desk_item(self, publish=True):
if frappe.db.get_value("Item", self.item_code, "published_in_website") and publish: if frappe.db.get_value("Item", self.item_code, "published_in_website") and publish:
return # if already published don't publish again 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, 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)) '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): def validate_website_image(self):
if frappe.flags.in_import: if frappe.flags.in_import:
return return
@ -133,6 +159,164 @@ class WebsiteItem(WebsiteGenerator):
self.thumbnail = file_doc.thumbnail_url 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() @frappe.whitelist()
def make_website_item(doc): def make_website_item(doc):
if not 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")}): 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"))) 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 = frappe.new_doc("Website Item")
website_item.web_item_name = doc.get("item_name") website_item.web_item_name = doc.get("item_name")
fields_to_map = ["item_code", "item_name", "item_group", "stock_uom", "brand", "image", 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: for field in fields_to_map:
website_item.update({field: doc.get(field)}) website_item.update({field: doc.get(field)})

View File

@ -240,8 +240,8 @@ doc_events = {
"erpnext.support.doctype.issue.issue.set_first_response_time" "erpnext.support.doctype.issue.issue.set_first_response_time"
] ]
}, },
"Sales Taxes and Charges Template": { ("Sales Taxes and Charges Template", "Price List"): {
"on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings" "on_update": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.validate_cart_settings"
}, },
"Website Settings": { "Website Settings": {
"validate": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.home_page_is_products" "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.setup.doctype.item_group.item_group import get_child_groups
from erpnext.shopping_cart.product_info import get_product_info_for_website 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) @frappe.whitelist(allow_guest=True)
def get_attributes_and_values(item_code): def get_attributes_and_values(item_code):
'''Build a list of attributes and their possible values. '''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) 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 # utilities
def get_item_attributes(item_code): def get_item_attributes(item_code):

View File

@ -1,10 +1,8 @@
from __future__ import unicode_literals
import frappe import frappe
from frappe.utils.nestedset import get_root_of from frappe.utils.nestedset import get_root_of
from erpnext.shopping_cart.cart import get_debtors_account from erpnext.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, get_shopping_cart_settings,
) )

View File

@ -180,7 +180,7 @@ $.extend(shopping_cart, {
show_cart_navbar: function () { show_cart_navbar: function () {
frappe.call({ 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) { callback: function(r) {
$(".shopping-cart").toggleClass('hidden', r.message ? false : true); $(".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 = frappe.get_doc(customer_doclist)
customer.flags.ignore_permissions = ignore_permissions customer.flags.ignore_permissions = ignore_permissions
if quotation.get("party_name") == "Shopping Cart": 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") "default_customer_group")
try: try:

View File

@ -120,9 +120,8 @@ class ItemGroup(NestedSet, WebsiteGenerator):
values[f"slide_{index + 1}_image"] = slide.image values[f"slide_{index + 1}_image"] = slide.image
values[f"slide_{index + 1}_title"] = slide.heading values[f"slide_{index + 1}_title"] = slide.heading
values[f"slide_{index + 1}_subtitle"] = slide.description values[f"slide_{index + 1}_subtitle"] = slide.description
values[f"slide_{index + 1}_theme"] = slide.theme or "Light" values[f"slide_{index + 1}_theme"] = slide.get("theme") or "Light"
values[f"slide_{index + 1}_content_align"] = slide.content_align or "Centre" values[f"slide_{index + 1}_content_align"] = slide.get("content_align") or "Centre"
values[f"slide_{index + 1}_primary_action_label"] = slide.label
values[f"slide_{index + 1}_primary_action"] = slide.url values[f"slide_{index + 1}_primary_action"] = slide.url
context.slideshow = values 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 = frappe.db.sql(query, {"product_group": product_group,"search": search, "today": nowdate()}, as_dict=1)
data = adjust_qty_for_expired_items(data) 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: for item in data:
set_product_info_for_website(item) set_product_info_for_website(item)

View File

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

View File

@ -534,7 +534,7 @@ def create_bank_account(args):
pass pass
def update_shopping_cart_settings(args): 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({ shopping_cart.update({
"enabled": 1, "enabled": 1,
'company': args.company_name, 'company': args.company_name,

View File

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

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 # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe import frappe
from erpnext.shopping_cart.cart import _get_cart_quotation, _set_price_list 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, 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) @frappe.whitelist(allow_guest=True)
def get_product_info_for_website(item_code, skip_quotation_creation=False): 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 """Query engine for product listing
Attributes: Attributes:
cart_settings (Document): Settings for Cart
fields (list): Fields to fetch in query fields (list): Fields to fetch in query
filters (TYPE): Description conditions (string): Conditions for query building
or_filters (list): Description or_conditions (string): Search conditions
page_length (Int): Length of page for the query page_length (Int): Length of page for the query
settings (Document): E Commerce Settings DocType settings (Document): E Commerce Settings DocType
filters (list)
or_filters (list)
""" """
def __init__(self): def __init__(self):
self.settings = frappe.get_doc("E Commerce Settings") 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.page_length = self.settings.products_per_page or 20
self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants', self.fields = ['wi.name', 'wi.item_name', 'wi.item_code', 'wi.website_image', 'wi.variant_of',
'item_group', 'image', 'web_long_description', 'description', 'route', 'weightage'] 'wi.has_variants', 'wi.item_group', 'wi.image', 'wi.web_long_description', 'wi.description',
self.filters = [] 'wi.route']
self.or_filters = [['show_in_website', '=', 1]] self.conditions = ""
if not self.settings.get('hide_variants'): self.or_conditions = ""
self.or_filters.append(['show_variant_in_website', '=', 1]) self.substitutions = []
def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None): def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None):
"""Summary """Summary
@ -57,51 +53,14 @@ class ProductQuery:
filters=[["Website Item Group", "item_group", "=", item_group]] filters=[["Website Item Group", "item_group", "=", item_group]]
) )
self.query_fields = (", ").join(self.fields)
if attributes: if attributes:
all_items = [] result = self.query_items_with_attributes(attributes, start)
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))]
else: else:
result = frappe.get_all( result = self.query_items(self.conditions, self.or_conditions,
"Item", self.substitutions, start=start)
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)
# add price info in results
for item in result: for item in result:
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info') product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
if product_info: if product_info:
@ -109,6 +68,51 @@ class ProductQuery:
return result 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): def build_fields_filters(self, filters):
"""Build filters for field values """Build filters for field values
@ -130,10 +134,11 @@ class ProductQuery:
self.filters.append([child_doctype, fields[0].fieldname, 'IN', values]) self.filters.append([child_doctype, fields[0].fieldname, 'IN', values])
elif isinstance(values, list): elif isinstance(values, list):
# If value is a list use `IN` query # 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: else:
# `=` will be faster than `IN` for most cases # `=` 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): def build_search_filters(self, search_term):
"""Query search term in specified fields """Query search term in specified fields
@ -158,4 +163,5 @@ class ProductQuery:
# Build or filters for query # Build or filters for query
search = '%{}%'.format(search_term) 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 # helper functions
def enable_shopping_cart(self): def enable_shopping_cart(self):
settings = frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings") settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
settings.update({ settings.update({
"enabled": 1, "enabled": 1,
@ -197,7 +197,7 @@ class TestShoppingCart(unittest.TestCase):
frappe.local.shopping_cart_settings = None frappe.local.shopping_cart_settings = None
def disable_shopping_cart(self): def disable_shopping_cart(self):
settings = frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings") settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
settings.enabled = 0 settings.enabled = 0
settings.save() settings.save()
frappe.local.shopping_cart_settings = None frappe.local.shopping_cart_settings = None

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 # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe import frappe
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import ( from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import is_cart_enabled
is_cart_enabled,
)
def show_cart_count(): def show_cart_count():
if (is_cart_enabled() and if (is_cart_enabled() and

View File

@ -21,7 +21,6 @@ from frappe.utils import (
strip, strip,
) )
from frappe.utils.html_utils import clean_html 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.utils import clear_cache
from frappe.website.website_generator import WebsiteGenerator from frappe.website.website_generator import WebsiteGenerator
@ -131,8 +130,6 @@ class Item(WebsiteGenerator):
self.validate_attributes() self.validate_attributes()
self.validate_variant_attributes() self.validate_variant_attributes()
self.validate_variant_based_on_change() self.validate_variant_based_on_change()
self.validate_website_image()
self.make_thumbnail()
self.validate_fixed_asset() self.validate_fixed_asset()
self.validate_retain_sample() self.validate_retain_sample()
self.validate_uom_conversion_factor() self.validate_uom_conversion_factor()
@ -141,7 +138,6 @@ class Item(WebsiteGenerator):
self.validate_item_defaults() self.validate_item_defaults()
self.validate_auto_reorder_enabled_in_stock_settings() self.validate_auto_reorder_enabled_in_stock_settings()
self.cant_change() self.cant_change()
self.update_show_in_website()
self.validate_item_tax_net_rate_range() self.validate_item_tax_net_rate_range()
set_item_tax_from_hsn_code(self) set_item_tax_from_hsn_code(self)
@ -156,7 +152,6 @@ class Item(WebsiteGenerator):
self.validate_name_with_item_group() self.validate_name_with_item_group()
self.update_variants() self.update_variants()
self.update_item_price() self.update_item_price()
self.update_template_item()
def validate_description(self): def validate_description(self):
'''Clean HTML description if set''' '''Clean HTML description if set'''
@ -218,95 +213,6 @@ class Item(WebsiteGenerator):
stock_entry.add_comment("Comment", _("Opening Stock")) 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): def validate_fixed_asset(self):
if self.is_fixed_asset: if self.is_fixed_asset:
if self.is_stock_item: 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( frappe.throw(_("{0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format(
self.item_code)) 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): def add_default_uom_in_conversion_factor_table(self):
uom_conv_list = [d.uom for d in self.get("uoms")] uom_conv_list = [d.uom for d in self.get("uoms")]
if self.stock_uom not in uom_conv_list: if self.stock_uom not in uom_conv_list:
@ -505,10 +250,6 @@ class Item(WebsiteGenerator):
[self.remove(d) for d in to_remove] [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): def validate_item_tax_net_rate_range(self):
for tax in self.get('taxes'): for tax in self.get('taxes'):
if flt(tax.maximum_net_rate) < flt(tax.minimum_net_rate): if flt(tax.maximum_net_rate) < flt(tax.minimum_net_rate):
@ -678,7 +419,7 @@ class Item(WebsiteGenerator):
if merge: if merge:
self.validate_duplicate_item_in_stock_reconciliation(old_name, new_name) self.validate_duplicate_item_in_stock_reconciliation(old_name, new_name)
if self.route: if self.published_in_website:
invalidate_cache_for_item(self) invalidate_cache_for_item(self)
clear_cache(self.route) clear_cache(self.route)
@ -777,25 +518,6 @@ class Item(WebsiteGenerator):
where item_code = %s and docstatus < 2 where item_code = %s and docstatus < 2
""", (self.description, self.name)) """, (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): def validate_item_defaults(self):
companies = {row.company for row in self.item_defaults} companies = {row.company for row in self.item_defaults}
@ -1065,7 +787,6 @@ class Item(WebsiteGenerator):
'item_code': item, 'item_code': item,
'item_name': item, 'item_name': item,
'description': item, 'description': item,
'show_in_website': 1,
'is_sales_item': 1, 'is_sales_item': 1,
'is_purchase_item': 1, 'is_purchase_item': 1,
'is_stock_item': 1, 'is_stock_item': 1,

View File

@ -23,7 +23,7 @@
}) })
</script> </script>
{% else %} {% 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 %} {% endif %}
<!-- Simple image preview --> <!-- Simple image preview -->

View File

@ -4,7 +4,7 @@
{% set select_address = True %} {% set select_address = True %}
{% endif %} {% 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%} {% if show_coupon_code == 1%}
<div class="mb-3"> <div class="mb-3">
<div class="row no-gutters"> <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 }}"> <a class="product-link product-list-link" href="{{ route|abs_url }}">
<div class='row'> <div class='row'>

View File

@ -6,10 +6,7 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import ( from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import show_attachments
show_attachments,
)
def get_context(context): def get_context(context):
context.no_cache = 1 context.no_cache = 1
@ -26,7 +23,7 @@ def get_context(context):
context.payment_ref = frappe.db.get_value("Payment Request", context.payment_ref = frappe.db.get_value("Payment Request",
{"reference_name": frappe.form_dict.name}, "name") {"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") 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: if default_print_format:

View File

@ -129,7 +129,7 @@ $(() => {
Object.assign(field_filters, { item_group }); Object.assign(field_filters, { item_group });
} }
return new Promise((resolve, reject) => { 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 => { .then(r => {
if (r.exc) reject(r.exc); if (r.exc) reject(r.exc);
else resolve(r.message); else resolve(r.message);

View File

@ -1,9 +1,7 @@
import frappe import frappe
from frappe.utils import cint
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 erpnext.shopping_cart.product_query import ProductQuery from erpnext.shopping_cart.product_query import ProductQuery
from erpnext.shopping_cart.filters import ProductFiltersBuilder
sitemap = 1 sitemap = 1
@ -13,7 +11,7 @@ def get_context(context):
search = frappe.form_dict.search search = frappe.form_dict.search
field_filters = frappe.parse_json(frappe.form_dict.field_filters) field_filters = frappe.parse_json(frappe.form_dict.field_filters)
attribute_filters = frappe.parse_json(frappe.form_dict.attribute_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: else:
search = field_filters = attribute_filters = None search = field_filters = attribute_filters = None
start = 0 start = 0
@ -24,15 +22,34 @@ def get_context(context):
# Add homepage as parent # Add homepage as parent
context.parents = [{"name": frappe._("Home"), "route":"/"}] context.parents = [{"name": frappe._("Home"), "route":"/"}]
e_commerce_settings = get_e_commerce_settings()
filter_engine = ProductFiltersBuilder() filter_engine = ProductFiltersBuilder()
context.field_filters = filter_engine.get_field_filters() context.field_filters = filter_engine.get_field_filters()
context.attribute_filters = filter_engine.get_attribute_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.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 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