From a675af5cf8f70642b30402455182c7dddb3fb8e8 Mon Sep 17 00:00:00 2001 From: Casey Wittrock Date: Wed, 12 Nov 2025 09:00:11 -0600 Subject: [PATCH] fix count filtering with or statements --- custom_ui/api/db.py | 304 +++++------------------ custom_ui/db_utils.py | 43 +++- frontend/src/components/pages/Client.vue | 17 +- 3 files changed, 120 insertions(+), 244 deletions(-) diff --git a/custom_ui/api/db.py b/custom_ui/api/db.py index fd72f76..c9702b5 100644 --- a/custom_ui/api/db.py +++ b/custom_ui/api/db.py @@ -1,6 +1,6 @@ import frappe, json, re from datetime import datetime, date -from custom_ui.db_utils import process_filters, process_sorting +from custom_ui.db_utils import process_query_conditions, build_datatable_response, get_count_or_filters @frappe.whitelist() def get_client_status_counts(weekly=False, week_start_date=None, week_end_date=None): @@ -81,13 +81,11 @@ def get_client_status_counts(weekly=False, week_start_date=None, week_end_date=N @frappe.whitelist() def get_client(client_name): address = frappe.get_doc("Address", client_name) - customer_name = [link for link in address.links if link.link_doctype == "Customer"][0].link_name - project_names = frappe.db.get_all("Project", fields=["name"], filters=[ - ["or", [ - ["custom_installation_address", "=", address.address_title], - ["custom_address", "=", address.address_title] - ]] - ]) + customer_name = address.custom_customer_to_bill if address.custom_customer_to_bill else [link.link_name for link in address.links if link.link_doctype == "Customer"][0] if address.links else None + project_names = frappe.db.get_all("Project", fields=["name"], or_filters=[ + ["custom_installation_address", "=", address.address_title], + ["custom_address", "=", address.address_title] + ], limit_page_length=100) projects = [frappe.get_doc("Project", proj["name"]) for proj in project_names] projects_data = [] @@ -101,47 +99,43 @@ def get_client(client_name): @frappe.whitelist() -def get_clients_table_data(filters = {}, sorting = [], page=1, page_size=10): - print("DEBUG: Raw client table options received:", { +def get_clients_table_data(filters = {}, sortings = [], page=1, page_size=10): + print("DEBUG: Raw client table query received:", { "filters": filters, - "sorting": sorting, + "sortings": sortings, "page": page, "page_size": page_size }) - page = int(page) - page_size = int(page_size) - processed_filters = process_filters(filters) - print("DEBUG: Processed filters:", processed_filters) - - - processed_sortings = process_sorting(sorting) - print("DEBUG: Order by:", processed_sortings) - - count = frappe.db.count("Address", filters=processed_filters) - print("DEBUG: Total addresses count:", count) - print("DEBUG: Filters (repr):", repr(processed_filters), "Type:", type(processed_filters)) - print("DEBUG: Order by (repr):", repr(processed_sortings), "Type:", type(processed_sortings)) + processed_filters, processed_sortings, is_or, page, page_size = process_query_conditions(filters, sortings, page, page_size) + count = frappe.db.count( + "Address", + filters=processed_filters + ) if not is_or else frappe.db.sql( + *get_count_or_filters("Address", processed_filters) + )[0][0] + print("DEBUG: Count of addresses matching filters:", count) address_names = frappe.db.get_all( "Address", fields=["name"], - filters=processed_filters if isinstance(processed_filters, dict) else None, - or_filters=processed_filters if isinstance(processed_filters, list) else None, + filters=processed_filters if not is_or else None, + or_filters=processed_filters if is_or else None, limit=page_size, start=(page - 1) * page_size, order_by=processed_sortings ) - print("DEBUG: Retrieved address names:", address_names) - addresses = [frappe.get_doc("Address", addr["name"]).as_dict() for addr in address_names] - - rows = [] + tableRows = [] for address in addresses: - links = address.links - print("DEBUG: Address links:", links) - customer_links = [link for link in links if link.link_doctype == "Customer"] if links else None - print("DEBUG: Extracted customer links:", customer_links) - customer_name = address["custom_customer_to_bill"] if address.get("custom_customer_to_bill") else (customer_links[0].link_name if customer_links else "N/A") tableRow = {} + links = address.links + customer_links = [link for link in links if link.link_doctype == "Customer"] if links else None + customer_name = address.get("custom_customer_to_bill") + if not customer_name and not customer_links: + print("DEBUG: No customer links found and no customer to bill.") + customer_name = "N/A" + elif not customer_name and customer_links: + print("DEBUG: No customer to bill. Customer links found:", customer_links) + customer_name = customer_links[0].link_name tableRow["id"] = address["name"] tableRow["customer_name"] = customer_name tableRow["address"] = ( @@ -153,17 +147,8 @@ def get_clients_table_data(filters = {}, sorting = [], page=1, page_size=10): tableRow["estimate_sent_status"] = address.custom_estimate_sent_status tableRow["job_status"] = address.custom_job_status tableRow["payment_received_status"] = address.custom_payment_received_status - rows.append(tableRow) - - return { - "pagination": { - "total": count, - "page": page, - "page_size": page_size, - "total_pages": (count + page_size - 1) // page_size - }, - "data": rows - } + tableRows.append(tableRow) + return build_datatable_response(data=tableRows, count=count, page=page, page_size=page_size) @frappe.whitelist() @@ -215,6 +200,7 @@ def upsert_client(data): "country": "United States", "address_title": data.get("address_title"), "pincode": data.get("pincode"), + "custom_customer_to_bill": customer_doc.name }).insert(ignore_permissions=True) link = { "link_doctype": "Customer", @@ -230,197 +216,58 @@ def upsert_client(data): } @frappe.whitelist() -def get_jobs(options): - options = json.loads(options) - print("DEBUG: Raw job options received:", options) - defaultOptions = { - "fields": ["*"], - "filters": {}, - "sorting": {}, - "page": 1, - "page_size": 10, - "for_table": False - } - options = {**defaultOptions, **options} - print("DEBUG: Final job options:", options) - - jobs = [] - tableRows = [] - - # Process filters from PrimeVue format to Frappe format - processed_filters = {} - if options["filters"]: - for field_name, filter_obj in options["filters"].items(): - if isinstance(filter_obj, dict) and "value" in filter_obj: - if filter_obj["value"] is not None and filter_obj["value"] != "": - # Map frontend field names to backend field names - backend_field = map_job_field_name(field_name) - - # Handle different match modes - match_mode = filter_obj.get("matchMode", "contains") - if isinstance(match_mode, str): - match_mode = match_mode.lower() - - if match_mode in ("contains", "contains"): - processed_filters[backend_field] = ["like", f"%{filter_obj['value']}%"] - elif match_mode in ("startswith", "startsWith"): - processed_filters[backend_field] = ["like", f"{filter_obj['value']}%"] - elif match_mode in ("endswith", "endsWith"): - processed_filters[backend_field] = ["like", f"%{filter_obj['value']}"] - elif match_mode in ("equals", "equals"): - processed_filters[backend_field] = filter_obj["value"] - else: - # Default to contains - processed_filters[backend_field] = ["like", f"%{filter_obj['value']}%"] - - # Process sorting - order_by = None - if options.get("sorting") and options["sorting"]: - sorting_str = options["sorting"] - if sorting_str and sorting_str.strip(): - # Parse "field_name asc/desc" format - parts = sorting_str.strip().split() - if len(parts) >= 2: - sort_field = parts[0] - sort_direction = parts[1].lower() - # Map frontend field to backend field - backend_sort_field = map_job_field_name(sort_field) - order_by = f"{backend_sort_field} {sort_direction}" - - print("DEBUG: Processed job filters:", processed_filters) - print("DEBUG: Job order by:", order_by) - - count = frappe.db.count("Project", filters=processed_filters) - print("DEBUG: Total projects count:", count) - +def get_jobs_table_data(filters = {}, sortings = [], page=1, page_size=10): + print("DEBUG: Raw job options received:", filters, sortings, page, page_size) + processed_filters, processed_sortings, is_or, page, page_size = process_query_conditions(filters, sortings, page, page_size) + count = frappe.db.count( + "Project", + filters=processed_filters if not is_or else None, + or_filters=processed_filters if is_or else None, + ) if not is_or else frappe.db.sql( + build + ) projects = frappe.db.get_all( "Project", - fields=options["fields"], - filters=processed_filters, - limit=options["page_size"], - start=(options["page"] - 1) * options["page_size"], - order_by=order_by + fields=["*"], + filters=processed_filters if not is_or else None, + or_filters=processed_filters if is_or else None, + limit=page_size, + start=(page - 1) * page_size, + order_by=processed_sortings ) - + tableRows = [] for project in projects: - job = {} tableRow = {} - tableRow["id"] = project["name"] tableRow["name"] = project["name"] - tableRow["customInstallationAddress"] = project.get("custom_installation_address", "") + tableRow["installation_address"] = project.get("custom_installation_address", "") tableRow["customer"] = project.get("customer", "") tableRow["status"] = project.get("status", "") - tableRow["percentComplete"] = project.get("percent_complete", 0) - tableRows.append(tableRow) - - job["project"] = project - jobs.append(job) - - return { - "pagination": { - "total": count, - "page": options["page"], - "page_size": options["page_size"], - "total_pages": (count + options["page_size"] - 1) // options["page_size"] - }, - "data": tableRows if options["for_table"] else jobs - } + tableRow["percent_complete"] = project.get("percent_complete", 0) + tableRows.append(tableRow) + return build_datatable_response(data=tableRows, count=count, page=page, page_size=page_size) @frappe.whitelist() -def get_warranty_claims(options): - options = json.loads(options) - print("DEBUG: Raw warranty options received:", options) - defaultOptions = { - "fields": ["*"], - "filters": {}, - "sorting": {}, - "page": 1, - "page_size": 10, - "for_table": False - } - options = {**defaultOptions, **options} - print("DEBUG: Final warranty options:", options) - - warranties = [] - tableRows = [] - - # Map frontend field names to backend field names for Warranty Claim doctype - def map_warranty_field_name(frontend_field): - field_mapping = { - "warrantyId": "name", - "customer": "customer_name", - "serviceAddress": "service_address", - "complaint": "complaint", - "status": "status", - "complaintDate": "complaint_date", - "complaintRaisedBy": "complaint_raised_by", - "fromCompany": "from_company", - "territory": "territory", - "resolutionDate": "resolution_date", - "warrantyStatus": "warranty_amc_status" - } - return field_mapping.get(frontend_field, frontend_field) - - # Process filters from PrimeVue format to Frappe format - processed_filters = {} - if options["filters"]: - for field_name, filter_obj in options["filters"].items(): - if isinstance(filter_obj, dict) and "value" in filter_obj: - if filter_obj["value"] is not None and filter_obj["value"] != "": - # Map frontend field names to backend field names - backend_field = map_warranty_field_name(field_name) - - # Handle different match modes - match_mode = filter_obj.get("matchMode", "contains") - if isinstance(match_mode, str): - match_mode = match_mode.lower() - - if match_mode in ("contains", "contains"): - processed_filters[backend_field] = ["like", f"%{filter_obj['value']}%"] - elif match_mode in ("startswith", "startsWith"): - processed_filters[backend_field] = ["like", f"{filter_obj['value']}%"] - elif match_mode in ("endswith", "endsWith"): - processed_filters[backend_field] = ["like", f"%{filter_obj['value']}"] - elif match_mode in ("equals", "equals"): - processed_filters[backend_field] = filter_obj["value"] - else: - # Default to contains - processed_filters[backend_field] = ["like", f"%{filter_obj['value']}%"] - - # Process sorting - order_by = None - if options.get("sorting") and options["sorting"]: - sorting_str = options["sorting"] - if sorting_str and sorting_str.strip(): - # Parse "field_name asc/desc" format - parts = sorting_str.strip().split() - if len(parts) >= 2: - sort_field = parts[0] - sort_direction = parts[1].lower() - # Map frontend field to backend field - backend_sort_field = map_warranty_field_name(sort_field) - order_by = f"{backend_sort_field} {sort_direction}" - - print("DEBUG: Processed warranty filters:", processed_filters) - print("DEBUG: Warranty order by:", order_by) - - count = frappe.db.count("Warranty Claim", filters=processed_filters) - print("DEBUG: Total warranty claims count:", count) - +def get_warranty_claims(filters = {}, sortings = [], page=1, page_size=10): + print("DEBUG: Raw warranty options received:", filters, sortings, page, page_size) + processed_filters, processed_sortings, is_or, page, page_size = process_query_conditions(filters, sortings, page, page_size) + count = frappe.db.count( + "Warranty Claim", + filters=processed_filters if not is_or else None, + or_filters=processed_filters if is_or else None + ) warranty_claims = frappe.db.get_all( "Warranty Claim", - fields=options["fields"], - filters=processed_filters, - limit=options["page_size"], - start=(options["page"] - 1) * options["page_size"], - order_by=order_by + fields=["*"], + filters=processed_filters if not is_or else None, + or_filters=processed_filters if is_or else None, + limit=page_size, + start=(page - 1) * page_size, + order_by=processed_sortings ) - + tableRows = [] for warranty in warranty_claims: - warranty_obj = {} tableRow = {} - tableRow["id"] = warranty["name"] tableRow["warrantyId"] = warranty["name"] tableRow["customer"] = warranty.get("customer_name", "") @@ -467,18 +314,5 @@ def get_warranty_claims(options): tableRow["priority"] = "Medium" else: tableRow["priority"] = "Low" - tableRows.append(tableRow) - - warranty_obj["warranty_claim"] = warranty - warranties.append(warranty_obj) - - return { - "pagination": { - "total": count, - "page": options["page"], - "page_size": options["page_size"], - "total_pages": (count + options["page_size"] - 1) // options["page_size"] - }, - "data": tableRows if options["for_table"] else warranties - } + return build_datatable_response(data=tableRows, count=count, page=page, page_size=page_size) \ No newline at end of file diff --git a/custom_ui/db_utils.py b/custom_ui/db_utils.py index 52bb1d3..80b9dd3 100644 --- a/custom_ui/db_utils.py +++ b/custom_ui/db_utils.py @@ -10,6 +10,10 @@ def map_field_name(frontend_field): "payment_received_status": "custom_payment_received_status", "job_status": "custom_job_status", "installation_address": "custom_installation_address", + "warranty_id": "name", + "customer": "customer_name", + "fromCompany": "from_company", + "warranty_status": "warranty_amc_status" } return field_mapping.get(frontend_field, frontend_field) @@ -58,6 +62,7 @@ def process_filters(filters): else: # Default to contains processed_filters[mapped_field_name] = ["like", f"%{filter_obj['value']}%"] + print("DEBUG: Processed filters:", processed_filters) return processed_filters def process_sorting(sortings): @@ -71,4 +76,40 @@ def process_sorting(sortings): order_by = order_by.rstrip(", ") else: order_by = "modified desc" - return order_by \ No newline at end of file + print("DEBUG: Processed sorting:", order_by) + return order_by + + +def process_query_conditions(filters, sortings, page, page_size): + processed_filters = process_filters(filters) + processed_sortings = process_sorting(sortings) + is_or_filters = isinstance(processed_filters, list) + page_int = int(page) + page_size_int = int(page_size) + return processed_filters, processed_sortings, is_or_filters, page_int, page_size_int + + +def build_datatable_response(data, count, page, page_size): + return { + "pagination": { + "total": count, + "page": page, + "page_size": page_size, + "total_pages": (count + page_size - 1) // page_size + }, + "data": data + } + +def get_count_or_filters(doctype, or_filters): + where_clauses = [] + values = [] + for field, operator, val in or_filters: + if operator.lower() == "like": + where_clauses.append(f"`{field}` LIKE %s") + else: + where_clauses.append(f"`{field}` {operator} %s") + values.append(val) + where_sql = " OR ".join(where_clauses) + sql = f"SELECT COUNT(*) FROM `tab{doctype}` WHERE {where_sql}" + return sql, values + \ No newline at end of file diff --git a/frontend/src/components/pages/Client.vue b/frontend/src/components/pages/Client.vue index ab23b8e..fa5d71f 100644 --- a/frontend/src/components/pages/Client.vue +++ b/frontend/src/components/pages/Client.vue @@ -29,31 +29,32 @@