From eef9cf152f1db8fcc8cfa23e8f51a7735c70637b Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 16 Feb 2021 18:45:36 +0530 Subject: [PATCH] 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 --- .../payment_request/payment_request.py | 4 +- erpnext/accounts/doctype/tax_rule/tax_rule.py | 2 +- .../e_commerce_settings.js | 38 ++- .../e_commerce_settings.json | 184 ++++++++++- .../e_commerce_settings.py | 83 ++++- .../test_e_commerce_settings.py | 37 ++- .../doctype/website_item/website_item.json | 27 +- .../doctype/website_item/website_item.py | 190 ++++++++++- erpnext/hooks.py | 4 +- erpnext/portal/product_configurator/utils.py | 294 ------------------ erpnext/portal/utils.py | 4 +- erpnext/public/js/shopping_cart.js | 2 +- .../selling/doctype/quotation/quotation.py | 2 +- .../setup/doctype/item_group/item_group.py | 7 +- .../setup_wizard/operations/company_setup.py | 2 +- .../operations/install_fixtures.py | 2 +- erpnext/shopping_cart/cart.py | 20 +- .../shopping_cart_settings/__init__.py | 0 .../shopping_cart_settings.js | 38 --- .../shopping_cart_settings.json | 212 ------------- .../shopping_cart_settings.py | 85 ----- .../test_shopping_cart_settings.py | 56 ---- erpnext/shopping_cart/product_info.py | 9 +- erpnext/shopping_cart/product_query.py | 120 +++---- erpnext/shopping_cart/test_shopping_cart.py | 4 +- erpnext/shopping_cart/utils.py | 9 +- erpnext/stock/doctype/item/item.py | 281 +---------------- .../templates/generators/item/item_image.html | 2 +- .../templates/includes/cart/cart_address.html | 2 +- .../templates/includes/products_as_list.html | 2 +- erpnext/templates/pages/order.py | 7 +- erpnext/www/all-products/index.js | 2 +- erpnext/www/all-products/index.py | 35 ++- 33 files changed, 667 insertions(+), 1099 deletions(-) delete mode 100644 erpnext/shopping_cart/doctype/shopping_cart_settings/__init__.py delete mode 100644 erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js delete mode 100644 erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json delete mode 100644 erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py delete mode 100644 erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 2c967497d5..c5b8b548cc 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -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}) diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.py b/erpnext/accounts/doctype/tax_rule/tax_rule.py index 150498d6ac..9a63dfedbe 100644 --- a/erpnext/accounts/doctype/tax_rule/tax_rule.py +++ b/erpnext/accounts/doctype/tax_rule/tax_rule.py @@ -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 diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js index d970f041be..131a5e439d 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js @@ -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( + `
${__("Follow these steps to create a landing page for your store")}: + + docs/store-landing-page + +
` + ); + } + + 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', ''); + } } }); diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json index 1a45adf6cd..b1b1cae770 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json @@ -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", diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py index 90596d6b6f..3fabc1a9b5 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py @@ -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.""" diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py index cf23266a29..798529b222 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py +++ b/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py @@ -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"] diff --git a/erpnext/e_commerce/doctype/website_item/website_item.json b/erpnext/e_commerce/doctype/website_item/website_item.json index 85a83e6d6e..02717eae14 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.json +++ b/erpnext/e_commerce/doctype/website_item/website_item.json @@ -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", diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index 1e0b12b4d6..55436f257d 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -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)}) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 684b13ab65..d0142d3d0d 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -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" diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index b0c5c3754b..0de97294f3 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -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): diff --git a/erpnext/portal/utils.py b/erpnext/portal/utils.py index bae8f353b3..3ee2c88939 100644 --- a/erpnext/portal/utils.py +++ b/erpnext/portal/utils.py @@ -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, ) diff --git a/erpnext/public/js/shopping_cart.js b/erpnext/public/js/shopping_cart.js index 6a923ae423..227881ac2a 100644 --- a/erpnext/public/js/shopping_cart.js +++ b/erpnext/public/js/shopping_cart.js @@ -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); } diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 99c43bfc87..e9644cc722 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -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: diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 2f03ee7183..3965d82751 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -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) diff --git a/erpnext/setup/setup_wizard/operations/company_setup.py b/erpnext/setup/setup_wizard/operations/company_setup.py index bea3906eba..be94994440 100644 --- a/erpnext/setup/setup_wizard/operations/company_setup.py +++ b/erpnext/setup/setup_wizard/operations/company_setup.py @@ -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}), diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index c473395a9a..fbfcb102dd 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -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, diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py index 3f1dfde016..cd9f1e8767 100644 --- a/erpnext/shopping_cart/cart.py +++ b/erpnext/shopping_cart/cart.py @@ -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 = '' diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/__init__.py b/erpnext/shopping_cart/doctype/shopping_cart_settings/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js deleted file mode 100644 index b38828e0d7..0000000000 --- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js +++ /dev/null @@ -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( - `
${__("Follow these steps to create a landing page for your store")}: - - docs/store-landing-page - -
` - ); - } - }, - 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', ''); - } - } -}); diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json deleted file mode 100644 index 7a4bb20136..0000000000 --- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py deleted file mode 100644 index 8f4afda57e..0000000000 --- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py +++ /dev/null @@ -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 diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py b/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py deleted file mode 100644 index f8a22b0e02..0000000000 --- a/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py +++ /dev/null @@ -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"] diff --git a/erpnext/shopping_cart/product_info.py b/erpnext/shopping_cart/product_info.py index fa6863696a..dd77536535 100644 --- a/erpnext/shopping_cart/product_info.py +++ b/erpnext/shopping_cart/product_info.py @@ -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): diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index d105ab898b..140e1c6b38 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -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) diff --git a/erpnext/shopping_cart/test_shopping_cart.py b/erpnext/shopping_cart/test_shopping_cart.py index d1284cdf00..63166c6ab7 100644 --- a/erpnext/shopping_cart/test_shopping_cart.py +++ b/erpnext/shopping_cart/test_shopping_cart.py @@ -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 diff --git a/erpnext/shopping_cart/utils.py b/erpnext/shopping_cart/utils.py index f412e61f06..98b522994a 100644 --- a/erpnext/shopping_cart/utils.py +++ b/erpnext/shopping_cart/utils.py @@ -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 diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 8cc9f74a42..2de4689f27 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -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, diff --git a/erpnext/templates/generators/item/item_image.html b/erpnext/templates/generators/item/item_image.html index 39a30d0d7c..e9b0916a70 100644 --- a/erpnext/templates/generators/item/item_image.html +++ b/erpnext/templates/generators/item/item_image.html @@ -23,7 +23,7 @@ }) {% 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 %} diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html index 4482bc10cf..979298fa8d 100644 --- a/erpnext/templates/includes/cart/cart_address.html +++ b/erpnext/templates/includes/cart/cart_address.html @@ -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%}
diff --git a/erpnext/templates/includes/products_as_list.html b/erpnext/templates/includes/products_as_list.html index 9bf9fd95d7..976d6147bc 100644 --- a/erpnext/templates/includes/products_as_list.html +++ b/erpnext/templates/includes/products_as_list.html @@ -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 %}
diff --git a/erpnext/templates/pages/order.py b/erpnext/templates/pages/order.py index d4e81ab096..59df433804 100644 --- a/erpnext/templates/pages/order.py +++ b/erpnext/templates/pages/order.py @@ -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: diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js index 1c641b59ad..37e07f4c7e 100644 --- a/erpnext/www/all-products/index.js +++ b/erpnext/www/all-products/index.js @@ -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); diff --git a/erpnext/www/all-products/index.py b/erpnext/www/all-products/index.py index 67d51ca161..688c02935a 100644 --- a/erpnext/www/all-products/index.py +++ b/erpnext/www/all-products/index.py @@ -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