Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add listview filters using specifications [WIP] #45

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
import time

import frappe
from dateutil.relativedelta import relativedelta
from frappe.core.doctype.doctype.doctype import no_value_fields, table_fields
from frappe.model.document import Document
from frappe.utils.data import flt, get_datetime, getdate
from frappe.utils.data import get_datetime
from pytz import timezone


Expand Down Expand Up @@ -40,7 +39,9 @@ def create_linked_values(self, doc, extra_attributes=None):
if at.date_values:
av.value = convert_to_epoch(av.value)
av.save()
if extra_attributes and at.attribute_name in extra_attributes:
if not extra_attributes:
continue
if at.attribute_name in extra_attributes:
if isinstance(extra_attributes[at.attribute_name], (str, int, float)):
existing_attribute_value = frappe.db.get_value(
"Specification Value",
Expand All @@ -62,10 +63,6 @@ def create_linked_values(self, doc, extra_attributes=None):
av.value = convert_to_epoch(av.value)
av.save()
continue

if not extra_attributes:
continue

for value in extra_attributes[at.attribute_name]: # list, tuple or set / not dict
existing_attribute_value = frappe.db.get_value(
"Specification Value",
Expand Down Expand Up @@ -102,7 +99,6 @@ def applies_to(self, doc):


def convert_to_epoch(date):
system_settings = frappe.get_cached_doc("System Settings", "System Settings")
d = datetime.datetime.now(
timezone(time.tzname if isinstance(time.tzname, (int, str)) else time.tzname[0])
) # or some other local date )
Expand Down
332 changes: 177 additions & 155 deletions inventory_tools/inventory_tools/faceted_search.py
Original file line number Diff line number Diff line change
@@ -1,155 +1,177 @@
import json
from time import localtime

import frappe
from erpnext.e_commerce.api import *
from frappe.utils.data import flt, getdate


@frappe.whitelist(allow_guest=True)
def show_faceted_search_components(doctype="Item", filters=None):
attributes = frappe.get_all(
"Specification Attribute",
{"applied_on": doctype},
["component", "attribute_name", "numeric_values", "date_values", "name AS attribute_id"],
order_by="idx ASC",
)

for attribute in attributes:
values = list(
set(
frappe.get_all(
"Specification Value",
{"attribute": attribute.attribute_name, "reference_doctype": doctype},
pluck="value",
)
)
)
if attribute.numeric_values and values:
_values = [flt(v) for v in values]
_min, _max = min(_values), max(_values)
attribute.values = [_min, _max]
elif attribute.date_values and values:
# TODO: convert from epoch
_values = [localtime(int(v)) for v in values]
_min, _max = min(_values), max(_values)
else:
attribute.values = values

return attributes


class FacetedSearchQuery(ProductQuery):
def query_items_with_attributes(self, attributes, start=0):
item_codes = []

attributes_in_use = {k: v for (k, v) in attributes.items() if v}
for attribute, spec_and_values in attributes_in_use.items():
spec = spec_and_values.get("attribute_id")
values = spec_and_values.get("values")
if not isinstance(values, list):
values = [values]
filters = None

date_or_numeric = frappe.get_value(
"Specification Attribute", spec, ["numeric_values", "date_values"]
)
if date_or_numeric[0] == 1:
filters = [
["attribute", "=", attribute],
["value", ">=", flt(values[0])],
["value", "<=", flt(values[-1])],
]
elif date_or_numeric[1] == 1:
filters = [
["attribute", "=", attribute],
["value", ">=", getdate(values[0]) if values[0] else "1900-1-1"],
["value", "<=", getdate(values[-1]) if values[-1] else "2100-12-31"],
]
else:
filters = {
"attribute": attribute,
"value": ["in", values],
}
print(values, filters)
item_code_list = frappe.get_all(
"Specification Value", fields=["reference_name"], filters=filters, debug=True
)
item_codes.append({x.reference_name for x in item_code_list})
if item_codes:
item_codes = list(set.intersection(*item_codes))
self.filters.append(["item_code", "in", item_codes])

return self.query_items(start=start)


@frappe.whitelist(allow_guest=True)
def get_product_filter_data(query_args=None):
if isinstance(query_args, str):
query_args = json.loads(query_args)

query_args = frappe._dict(query_args)

if query_args:
search = query_args.get("search")
field_filters = query_args.get("field_filters", {})
attribute_filters = query_args.get("attributes", {})
start = cint(query_args.start) if query_args.get("start") else 0
item_group = query_args.get("item_group")
from_filters = query_args.get("from_filters")
else:
search, attribute_filters, item_group, from_filters = None, None, None, None
field_filters = {}
start = 0

if from_filters:
start = 0

sub_categories = []
if item_group:
sub_categories = get_child_groups_for_website(item_group, immediate=True)

engine = FacetedSearchQuery()

try:
result = engine.query(
attribute_filters, field_filters, search_term=search, start=start, item_group=item_group
)
except Exception:
frappe.log_error("Product query with filter failed")
return {"exc": "Something went wrong!"}

filters = {}
discounts = result["discounts"]

if discounts:
filter_engine = ProductFiltersBuilder()
filters["discount_filters"] = filter_engine.get_discount_filters(discounts)

return {
"items": result["items"] or [],
"filters": filters,
"settings": engine.settings,
"sub_categories": sub_categories,
"items_count": result["items_count"],
}


@frappe.whitelist()
def update_specification_attribute_values(doc, method=None):
specifications = list(
set(
frappe.get_all(
"Specification Attribute",
fields=["parent"],
filters={"applied_on": doc.doctype},
pluck="parent",
)
)
)
if not specifications:
return
for spec in specifications:
spec = frappe.get_doc("Specification", spec)
if spec.applies_to(doc):
spec.create_linked_values(doc)
import json
from time import localtime

import frappe
from erpnext.e_commerce.api import *
from frappe.utils.data import flt, getdate


@frappe.whitelist(allow_guest=True)
def show_faceted_search_components(doctype="Item", filters=None):
attributes = frappe.get_all(
"Specification Attribute",
{"applied_on": doctype},
[
"component",
"attribute_name",
"numeric_values",
"date_values",
"field",
"name AS attribute_id",
],
order_by="idx ASC",
)

for attribute in attributes:
values = list(
set(
frappe.get_all(
"Specification Value",
{"attribute": attribute.attribute_name, "reference_doctype": doctype},
pluck="value",
)
)
)
if attribute.numeric_values and values:
_values = [flt(v) for v in values]
_min, _max = min(_values), max(_values)
attribute.values = [_min, _max]
elif attribute.date_values and values:
# TODO: convert from epoch
_values = [localtime(int(v)) for v in values]
_min, _max = min(_values), max(_values)
else:
attribute.values = values

return attributes


class FacetedSearchQuery(ProductQuery):
def query_items_with_attributes(self, attributes, start=0):
item_codes = []

attributes_in_use = {k: v for (k, v) in attributes.items() if v}
for attribute, spec_and_values in attributes_in_use.items():
spec = spec_and_values.get("attribute_id")
values = spec_and_values.get("values")
if not isinstance(values, list):
values = [values]
filters = None

date_or_numeric = frappe.get_value(
"Specification Attribute", spec, ["numeric_values", "date_values"]
)
if date_or_numeric[0] == 1:
filters = [
["attribute", "=", attribute],
["value", ">=", flt(values[0])],
["value", "<=", flt(values[-1])],
]
elif date_or_numeric[1] == 1:
filters = [
["attribute", "=", attribute],
["value", ">=", getdate(values[0]) if values[0] else "1900-1-1"],
["value", "<=", getdate(values[-1]) if values[-1] else "2100-12-31"],
]
else:
filters = {
"attribute": attribute,
"value": ["in", values],
}
print(values, filters)
item_code_list = frappe.get_all(
"Specification Value", fields=["reference_name"], filters=filters, debug=True
)
item_codes.append({x.reference_name for x in item_code_list})
if item_codes:
item_codes = list(set.intersection(*item_codes))
self.filters.append(["item_code", "in", item_codes])

return self.query_items(start=start)


@frappe.whitelist()
def get_specification_items(doctype: str, attribute_name: str, attribute_values: list[str]):
attribute_values = json.loads(attribute_values)
return frappe.get_all(
"Specification Value",
filters=[
["reference_doctype", "=", doctype],
["attribute", "=", attribute_name],
["value", ">=", attribute_values[0]],
["value", "<=", attribute_values[1]],
],
pluck="reference_name",
)


@frappe.whitelist(allow_guest=True)
def get_product_filter_data(query_args=None):
if isinstance(query_args, str):
query_args = json.loads(query_args)

query_args = frappe._dict(query_args)

if query_args:
search = query_args.get("search")
field_filters = query_args.get("field_filters", {})
attribute_filters = query_args.get("attributes", {})
start = cint(query_args.start) if query_args.get("start") else 0
item_group = query_args.get("item_group")
from_filters = query_args.get("from_filters")
else:
search, attribute_filters, item_group, from_filters = None, None, None, None
field_filters = {}
start = 0

if from_filters:
start = 0

sub_categories = []
if item_group:
sub_categories = get_child_groups_for_website(item_group, immediate=True)

engine = FacetedSearchQuery()

try:
result = engine.query(
attribute_filters, field_filters, search_term=search, start=start, item_group=item_group
)
except Exception:
frappe.log_error("Product query with filter failed")
return {"exc": "Something went wrong!"}

filters = {}
discounts = result["discounts"]

if discounts:
filter_engine = ProductFiltersBuilder()
filters["discount_filters"] = filter_engine.get_discount_filters(discounts)

return {
"items": result["items"] or [],
"filters": filters,
"settings": engine.settings,
"sub_categories": sub_categories,
"items_count": result["items_count"],
}


@frappe.whitelist()
def update_specification_attribute_values(doc, method=None):
specifications = list(
set(
frappe.get_all(
"Specification Attribute",
fields=["parent"],
filters={"applied_on": doc.doctype},
pluck="parent",
)
)
)
if not specifications:
return
for spec in specifications:
spec = frappe.get_doc("Specification", spec)
if spec.applies_to(doc):
spec.create_linked_values(doc)
Loading
Loading