Merge pull request #30733 from marination/field-filters-e-com

fix: Filter fields in E Commerce Settings (use Website Item)
This commit is contained in:
Ankush Menat
2022-04-21 14:30:31 +05:30
committed by GitHub
10 changed files with 226 additions and 131 deletions

View File

@@ -24,17 +24,16 @@ frappe.ui.form.on("E Commerce Settings", {
); );
} }
frappe.model.with_doctype("Item", () => { frappe.model.with_doctype("Website Item", () => {
const web_item_meta = frappe.get_meta('Website Item'); const web_item_meta = frappe.get_meta('Website Item');
const valid_fields = web_item_meta.fields.filter( const valid_fields = web_item_meta.fields.filter(df =>
df => ["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden ["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(
'fieldname', 'fieldtype', 'Select'
); );
frm.fields_dict.filter_fields.grid.update_docfield_property(
frm.get_field("filter_fields").grid.update_docfield_property(
'fieldname', 'options', valid_fields 'fieldname', 'options', valid_fields
); );
}); });

View File

@@ -27,7 +27,7 @@ class ECommerceSettings(Document):
self.is_redisearch_loaded = is_search_module_loaded() self.is_redisearch_loaded = is_search_module_loaded()
def validate(self): def validate(self):
self.validate_field_filters() self.validate_field_filters(self.filter_fields, self.enable_field_filters)
self.validate_attribute_filters() self.validate_attribute_filters()
self.validate_checkout() self.validate_checkout()
self.validate_search_index_fields() self.validate_search_index_fields()
@@ -51,21 +51,22 @@ class ECommerceSettings(Document):
define_autocomplete_dictionary() define_autocomplete_dictionary()
create_website_items_index() create_website_items_index()
def validate_field_filters(self): @staticmethod
if not (self.enable_field_filters and self.filter_fields): def validate_field_filters(filter_fields, enable_field_filters):
if not (enable_field_filters and filter_fields):
return return
item_meta = frappe.get_meta("Item") web_item_meta = frappe.get_meta("Website Item")
valid_fields = [ valid_fields = [
df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"] df.fieldname for df in web_item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]
] ]
for f in self.filter_fields: for row in filter_fields:
if f.fieldname not in valid_fields: if row.fieldname not in valid_fields:
frappe.throw( frappe.throw(
_( _(
"Filter Fields Row #{0}: Fieldname <b>{1}</b> must be of type 'Link' or 'Table MultiSelect'" "Filter Fields Row #{0}: Fieldname {1} must be of type 'Link' or 'Table MultiSelect'"
).format(f.idx, f.fieldname) ).format(row.idx, frappe.bold(row.fieldname))
) )
def validate_attribute_filters(self): def validate_attribute_filters(self):

View File

@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*- # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest import unittest
@@ -11,44 +10,35 @@ from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
class TestECommerceSettings(unittest.TestCase): class TestECommerceSettings(unittest.TestCase):
def setUp(self): def tearDown(self):
frappe.db.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """) frappe.db.rollback()
def get_cart_settings(self):
return frappe.get_doc({"doctype": "E Commerce 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_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): def test_tax_rule_validation(self):
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0") frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
frappe.db.commit() # nosemgrep frappe.db.commit() # nosemgrep
cart_settings = self.get_cart_settings() cart_settings = frappe.get_doc("E Commerce Settings")
cart_settings.enabled = 1 cart_settings.enabled = 1
if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"): if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"):
self.assertRaises(ShoppingCartSetupError, cart_settings.validate_tax_rule) self.assertRaises(ShoppingCartSetupError, cart_settings.validate_tax_rule)
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1") frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1")
def test_invalid_filter_fields(self):
"Check if Item fields are blocked in E Commerce Settings filter fields."
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
setup_e_commerce_settings({"enable_field_filters": 1})
create_custom_field(
"Item",
dict(owner="Administrator", fieldname="test_data", label="Test", fieldtype="Data"),
)
settings = frappe.get_doc("E Commerce Settings")
settings.append("filter_fields", {"fieldname": "test_data"})
self.assertRaises(frappe.ValidationError, settings.save)
def setup_e_commerce_settings(values_dict): def setup_e_commerce_settings(values_dict):
"Accepts a dict of values that updates E Commerce Settings." "Accepts a dict of values that updates E Commerce Settings."

View File

@@ -22,12 +22,14 @@ class ProductFiltersBuilder:
fields, filter_data = [], [] fields, filter_data = [], []
filter_fields = [row.fieldname for row in self.doc.filter_fields] # fields in settings filter_fields = [row.fieldname for row in self.doc.filter_fields] # fields in settings
# filter valid field filters i.e. those that exist in Item # filter valid field filters i.e. those that exist in Website Item
item_meta = frappe.get_meta("Item", cached=True) web_item_meta = frappe.get_meta("Website Item", cached=True)
fields = [item_meta.get_field(field) for field in filter_fields if item_meta.has_field(field)] fields = [
web_item_meta.get_field(field) for field in filter_fields if web_item_meta.has_field(field)
]
for df in fields: for df in fields:
item_filters, item_or_filters = {"published_in_website": 1}, [] item_filters, item_or_filters = {"published": 1}, []
link_doctype_values = self.get_filtered_link_doctype_records(df) link_doctype_values = self.get_filtered_link_doctype_records(df)
if df.fieldtype == "Link": if df.fieldtype == "Link":
@@ -50,9 +52,13 @@ class ProductFiltersBuilder:
] ]
) )
# exclude variants if mentioned in settings
if frappe.db.get_single_value("E Commerce Settings", "hide_variants"):
item_filters["variant_of"] = ["is", "not set"]
# Get link field values attached to published items # Get link field values attached to published items
item_values = frappe.get_all( item_values = frappe.get_all(
"Item", "Website Item",
fields=[df.fieldname], fields=[df.fieldname],
filters=item_filters, filters=item_filters,
or_filters=item_or_filters, or_filters=item_or_filters,

View File

@@ -277,6 +277,54 @@ class TestProductDataEngine(unittest.TestCase):
# tear down # tear down
setup_e_commerce_settings({"enable_attribute_filters": 1, "hide_variants": 0}) setup_e_commerce_settings({"enable_attribute_filters": 1, "hide_variants": 0})
def test_custom_field_as_filter(self):
"Test if custom field functions as filter correctly."
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
create_custom_field(
"Website Item",
dict(
owner="Administrator",
fieldname="supplier",
label="Supplier",
fieldtype="Link",
options="Supplier",
insert_after="on_backorder",
),
)
frappe.db.set_value(
"Website Item", {"item_code": "Test 11I Laptop"}, "supplier", "_Test Supplier"
)
frappe.db.set_value(
"Website Item", {"item_code": "Test 12I Laptop"}, "supplier", "_Test Supplier 1"
)
settings = frappe.get_doc("E Commerce Settings")
settings.append("filter_fields", {"fieldname": "supplier"})
settings.save()
filter_engine = ProductFiltersBuilder()
field_filters = filter_engine.get_field_filters()
custom_filter = field_filters[1]
filter_values = custom_filter[1]
self.assertEqual(custom_filter[0].options, "Supplier")
self.assertEqual(len(filter_values), 2)
self.assertIn("_Test Supplier", filter_values)
# test if custom filter works in query
field_filters = {"supplier": "_Test Supplier 1"}
engine = ProductQuery()
result = engine.query(
attributes={}, fields=field_filters, search_term=None, start=0, item_group=None
)
items = result.get("items")
# check if only 'Raw Material' are fetched in the right order
self.assertEqual(len(items), 1)
self.assertEqual(items[0].get("item_code"), "Test 12I Laptop")
def create_variant_web_item(): def create_variant_web_item():
"Create Variant and Template Website Items." "Create Variant and Template Website Items."

View File

@@ -364,3 +364,4 @@ erpnext.patches.v13_0.set_return_against_in_pos_invoice_references
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022 erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022
erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances
erpnext.patches.v13_0.create_gst_custom_fields_in_quotation erpnext.patches.v13_0.create_gst_custom_fields_in_quotation
erpnext.patches.v13_0.copy_custom_field_filters_to_website_item

View File

@@ -0,0 +1,94 @@
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
def execute():
"Add Field Filters, that are not standard fields in Website Item, as Custom Fields."
def move_table_multiselect_data(docfield):
"Copy child table data (Table Multiselect) from Item to Website Item for a docfield."
table_multiselect_data = get_table_multiselect_data(docfield)
field = docfield.fieldname
for row in table_multiselect_data:
# add copied multiselect data rows in Website Item
web_item = frappe.db.get_value("Website Item", {"item_code": row.parent})
web_item_doc = frappe.get_doc("Website Item", web_item)
child_doc = frappe.new_doc(docfield.options, web_item_doc, field)
for field in ["name", "creation", "modified", "idx"]:
row[field] = None
child_doc.update(row)
child_doc.parenttype = "Website Item"
child_doc.parent = web_item
child_doc.insert()
def get_table_multiselect_data(docfield):
child_table = frappe.qb.DocType(docfield.options)
item = frappe.qb.DocType("Item")
table_multiselect_data = ( # query table data for field
frappe.qb.from_(child_table)
.join(item)
.on(item.item_code == child_table.parent)
.select(child_table.star)
.where((child_table.parentfield == docfield.fieldname) & (item.published_in_website == 1))
).run(as_dict=True)
return table_multiselect_data
settings = frappe.get_doc("E Commerce Settings")
if not (settings.enable_field_filters or settings.filter_fields):
return
item_meta = frappe.get_meta("Item")
valid_item_fields = [
df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]
]
web_item_meta = frappe.get_meta("Website Item")
valid_web_item_fields = [
df.fieldname for df in web_item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]
]
for row in settings.filter_fields:
# skip if illegal field
if row.fieldname not in valid_item_fields:
continue
# if Item field is not in Website Item, add it as a custom field
if row.fieldname not in valid_web_item_fields:
df = item_meta.get_field(row.fieldname)
create_custom_field(
"Website Item",
dict(
owner="Administrator",
fieldname=df.fieldname,
label=df.label,
fieldtype=df.fieldtype,
options=df.options,
description=df.description,
read_only=df.read_only,
no_copy=df.no_copy,
insert_after="on_backorder",
),
)
# map field values
if df.fieldtype == "Table MultiSelect":
move_table_multiselect_data(df)
else:
frappe.db.sql( # nosemgrep
"""
UPDATE `tabWebsite Item` wi, `tabItem` i
SET wi.{0} = i.{0}
WHERE wi.item_code = i.item_code
""".format(
row.fieldname
)
)

View File

@@ -1,76 +1,31 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0, "creation": "2018-12-31 17:06:08.716134",
"allow_guest_to_view": 0, "doctype": "DocType",
"allow_import": 0, "editable_grid": 1,
"allow_rename": 0, "engine": "InnoDB",
"beta": 0, "field_order": [
"creation": "2018-12-31 17:06:08.716134", "fieldname"
"custom": 0, ],
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "fieldname",
"allow_in_quick_entry": 0, "fieldtype": "Autocomplete",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Fieldname"
"collapsible": 0,
"columns": 0,
"fieldname": "fieldname",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Fieldname",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "istable": 1,
"hide_heading": 0, "links": [],
"hide_toolbar": 0, "modified": "2022-04-18 18:55:17.835666",
"idx": 0, "modified_by": "Administrator",
"image_view": 0, "module": "Portal",
"in_create": 0, "name": "Website Filter Field",
"is_submittable": 0, "owner": "Administrator",
"issingle": 0, "permissions": [],
"istable": 1, "quick_entry": 1,
"max_attachments": 0, "sort_field": "modified",
"modified": "2019-01-01 18:26:11.550380", "sort_order": "DESC",
"modified_by": "Administrator", "states": [],
"module": "Portal", "track_changes": 1
"name": "Website Filter Field",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
} }

View File

@@ -72,17 +72,16 @@ frappe.ui.form.on("Item Group", {
}); });
} }
frappe.model.with_doctype('Item', () => { frappe.model.with_doctype('Website Item', () => {
const item_meta = frappe.get_meta('Item'); const web_item_meta = frappe.get_meta('Website Item');
const valid_fields = item_meta.fields.filter( const valid_fields = web_item_meta.fields.filter(df =>
df => ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden ['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(
'fieldname', 'fieldtype', 'Select'
); );
frm.fields_dict.filter_fields.grid.update_docfield_property(
frm.get_field("filter_fields").grid.update_docfield_property(
'fieldname', 'options', valid_fields 'fieldname', 'options', valid_fields
); );
}); });

View File

@@ -11,6 +11,7 @@ from frappe.utils.nestedset import NestedSet
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
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ECommerceSettings
from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder
@@ -35,6 +36,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
self.make_route() self.make_route()
self.validate_item_group_defaults() self.validate_item_group_defaults()
ECommerceSettings.validate_field_filters(self.filter_fields, enable_field_filters=True)
def on_update(self): def on_update(self):
NestedSet.on_update(self) NestedSet.on_update(self)