From 0b280cec8ee04dc4a30e241d70b0835a06e11b4b Mon Sep 17 00:00:00 2001 From: Casey Wittrock Date: Thu, 13 Nov 2025 15:17:43 -0600 Subject: [PATCH] add query for client details --- custom_ui/api/db/clients.py | 128 ++++++++++++++-------- custom_ui/db_utils.py | 1 + custom_ui/events/onsite_meeting.py | 12 +- custom_ui/events/quotation.py | 13 +-- frontend/src/api.js | 7 +- frontend/src/apiUtils.js | 2 +- frontend/src/components/pages/Client.vue | 18 +-- frontend/src/components/pages/Clients.vue | 2 +- 8 files changed, 114 insertions(+), 69 deletions(-) diff --git a/custom_ui/api/db/clients.py b/custom_ui/api/db/clients.py index 80bcf00..d6b6c71 100644 --- a/custom_ui/api/db/clients.py +++ b/custom_ui/api/db/clients.py @@ -92,50 +92,90 @@ def get_client_status_counts(weekly=False, week_start_date=None, week_end_date=N def get_client(client_name): """Get detailed information for a specific client including address, customer, and projects.""" try: - address = frappe.get_doc("Address", client_name) - 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 - if not customer_name: - raise Exception(f"No customer linked to address {client_name}. Suggested fix: Ensure the address is linked to a customer via the ERPnext UI.") - 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) - # contacts = [] # currently not needed as the customer doctype comes with contacts - onsite_meetings = frappe.db.get_all( - "On-Site Meeting", - fields=["*"], - filters={"address": address.address_title} - ) - quotations = frappe.db.get_all( - "Quotation", - fields=["*"], - filters={"custom_installation_address": address.address_title} - ) - sales_orders = [] - projects = [frappe.get_doc("Project", proj["name"]) for proj in project_names] - sales_invoices = [] - payment_entries = frappe.db.get_all( - doctype="Payment Entry", - fields=["*"], - filters={"party": customer_name}) - payment_orders = [] - jobs = [] - for project in projects: - job = [] - jobs.append(job) - customer = frappe.get_doc("Customer", customer_name) - # get all associated data as needed - return build_success_response({ - "address": address, - "customer": customer, - # "contacts": [], # currently not needed as the customer doctype comes with contacts - "jobs": jobs, - "sales_invoices": sales_invoices, - "payment_entries": payment_entries, - "sales_orders": sales_orders, - "quotations": quotations, - "onsite_meetings": onsite_meetings, - }) + clientData = {"addresses": []} + customer = frappe.get_doc("Customer", client_name) + clientData = {**clientData, **customer.as_dict()} + addresses = frappe.db.get_all("Address", fields=["*"], filters={"custom_customer_to_bill": client_name}) + for address in addresses if addresses else []: + addressData = {"jobs": []} + addressData = {**addressData, **address} + addressData["estimates"] = frappe.db.get_all("Quotation", fields=["*"], filters={"custom_installation_address": address.address_title}) + addressData["onsite_meetings"] = frappe.db.get_all("On-Site Meeting", fields=["*"], filters={"address": address.address_title}) + jobs = frappe.db.get_all("Project", fields=["*"], or_filters=[ + ["custom_installation_address", "=", address.address_title], + ["custom_address", "=", address.address_title] + ]) + for job in jobs if jobs else []: + jobData = {} + jobData = {**jobData, **job} + jobData["sales_invoices"] = frappe.db.get_all("Sales Invoice", fields=["*"], filters={"project": job.name}) + jobData["payment_entries"] = frappe.db.get_all( + "Payment Entry", + fields=["*"], + filters={"party_type": "Customer"}, + or_filters=[ + ["party", "=", client_name], + ["party_name", "=", client_name] + ]) + jobData["sales_orders"] = frappe.db.get_all("Sales Order", fields=["*"], filters={"project": job.name}) + jobData["tasks"] = frappe.db.get_all("Task", fields=["*"], filters={"project": job.name}) + addressData["jobs"].append(jobData) + clientData["addresses"].append(addressData) + return build_success_response(clientData) + except frappe.ValidationError as ve: + return build_error_response(str(ve), 400) + except Exception as e: + return build_error_response(str(e), 500) + + + # address = frappe.get_doc("Address", client_name) + # 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 + # if not customer_name: + # raise Exception(f"No customer linked to address {client_name}. Suggested fix: Ensure the address is linked to a customer via the ERPnext UI.") + # 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) + # # contacts = [] # currently not needed as the customer doctype comes with contacts + # onsite_meetings = frappe.db.get_all( + # "On-Site Meeting", + # fields=["*"], + # filters={"address": address.address_title} + # ) + # quotations = frappe.db.get_all( + # "Quotation", + # fields=["*"], + # filters={"custom_installation_address": address.address_title} + # ) + # sales_orders = [] + # projects = [frappe.get_doc("Project", proj["name"]) for proj in project_names] + # sales_invoices = [] + # payment_entries = frappe.db.get_all( + # "Payment Entry", + # fields=["*"], + # filters={"party_type": "Customer"}, + # or_filters=[ + # ["party", "=", customer_name], + # ["party_name", "=", customer_name] + # ]) + # payment_orders = [] + # jobs = [] + # for project in projects: + # job = [] + # jobs.append(job) + # customer = frappe.get_doc("Customer", customer_name) + # # get all associated data as needed + # return build_success_response({ + # "address": address, + # "customer": customer, + # # "contacts": [], # currently not needed as the customer doctype comes with contacts + # "jobs": jobs, + # "sales_invoices": sales_invoices, + # "payment_entries": payment_entries, + # "sales_orders": sales_orders, + # "quotations": quotations, + # "onsite_meetings": onsite_meetings, + # }) except frappe.ValidationError as ve: return build_error_response(str(ve), 400) except Exception as e: diff --git a/custom_ui/db_utils.py b/custom_ui/db_utils.py index 2128284..87814ae 100644 --- a/custom_ui/db_utils.py +++ b/custom_ui/db_utils.py @@ -68,6 +68,7 @@ def process_filters(filters): def process_sorting(sortings): sortings = json.loads(sortings) if isinstance(sortings, str) else sortings order_by = "" + print("DEBUG: Original sorting:", sortings) if sortings and len(sortings) > 0: for sorting in sortings: mapped_field = map_field_name(sorting[0].strip()) diff --git a/custom_ui/events/onsite_meeting.py b/custom_ui/events/onsite_meeting.py index be3904c..8c7bca0 100644 --- a/custom_ui/events/onsite_meeting.py +++ b/custom_ui/events/onsite_meeting.py @@ -2,10 +2,8 @@ import frappe def after_insert(doc, method): print(doc.address) - frappe.msgprint(f"On-Site Meeting '{doc.name}' has been created. Updating related records...") - address_name = frappe.db.get_value("Address", fieldname="name", filters={"address_line1": doc.address}) - address_doc = frappe.get_doc("Address", address_name) - frappe.msgprint(f"Related Address '{address_doc.address_title}' has been retrieved.") - address_doc.custom_onsite_meeting_scheduled = "Completed" - address_doc.save() - frappe.msgprint(f"Related Address '{address_doc.address_title}' has been updated with custom_onsite_meeting_scheduled = 'Completed'.") \ No newline at end of file + if doc.address: + address_name = frappe.db.get_value("Address", fieldname="name", filters={"address_line1": doc.address}) + address_doc = frappe.get_doc("Address", address_name) + address_doc.custom_onsite_meeting_scheduled = "Completed" + address_doc.save() \ No newline at end of file diff --git a/custom_ui/events/quotation.py b/custom_ui/events/quotation.py index d6b5334..cf4ec86 100644 --- a/custom_ui/events/quotation.py +++ b/custom_ui/events/quotation.py @@ -10,10 +10,9 @@ def after_insert(doc, method): def after_save(doc, method): - if doc.custome_sent: - address_title = doc.custom_installation_address - address_name = frappe.db.get_value("Address", fieldname="name", filters={"address_title": address_title}) - if address_name: - address_doc = frappe.get_doc("Address", address_name) - address_doc.custom_quotation_sent = "Completed" - address_doc.save() \ No newline at end of file + address_title = doc.custom_installation_address + address_name = frappe.db.get_value("Address", fieldname="name", filters={"address_title": address_title}) + if doc.custome_sent and address_name: + address_doc = frappe.get_doc("Address", address_name) + address_doc.custom_quotation_sent = "Completed" + address_doc.save() \ No newline at end of file diff --git a/frontend/src/api.js b/frontend/src/api.js index 8dc3451..ac7629e 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -1,4 +1,5 @@ import ApiUtils from "./apiUtils"; +import { useErrorStore } from "./stores/errors"; const ZIPPOPOTAMUS_BASE_URL = "https://api.zippopotam.us/us"; const FRAPPE_PROXY_METHOD = "custom_ui.api.proxy.request"; @@ -15,6 +16,7 @@ const FRAPPE_GET_CLIENT_NAMES_METHOD = "custom_ui.api.db.clients.get_client_name class Api { static async request(frappeMethod, args = {}) { + const errorStore = useErrorStore(); args = ApiUtils.toSnakeCaseObject(args); const request = { method: frappeMethod, args }; console.log("DEBUG: API - Request Args: ", request); @@ -28,6 +30,7 @@ class Api { return response.message.data; } catch (error) { console.error("ERROR: API - Request Error: ", error); + errorStore.setApiError("Frappe API", error.message || "API request error"); throw error; } } @@ -112,11 +115,11 @@ class Api { * @param {Object} sorting - Sorting parameters from store (optional) * @returns {Promise<{data: Array, pagination: Object}>} */ - static async getPaginatedClientDetails(paginationParams = {}, filters = {}, sorting = []) { + static async getPaginatedClientDetails(paginationParams = {}, filters = {}, sortings = []) { const { page = 0, pageSize = 10 } = paginationParams; const result = await this.request(FRAPPE_GET_CLIENT_TABLE_DATA_METHOD, { filters, - sorting, + sortings, page: page + 1, pageSize, }); diff --git a/frontend/src/apiUtils.js b/frontend/src/apiUtils.js index b60491b..add68dc 100644 --- a/frontend/src/apiUtils.js +++ b/frontend/src/apiUtils.js @@ -3,7 +3,7 @@ class ApiUtils { console.log("Converting to snake case:", obj); const newObj = Object.entries(obj).reduce((acc, [key, value]) => { const snakeKey = key.replace(/[A-Z]/g, (match) => "_" + match.toLowerCase()); - if (key === "sorting") { + if (key === "sortings") { value = value ? value.map((item) => { const [field, order] = item; diff --git a/frontend/src/components/pages/Client.vue b/frontend/src/components/pages/Client.vue index 2e6f664..93da045 100644 --- a/frontend/src/components/pages/Client.vue +++ b/frontend/src/components/pages/Client.vue @@ -37,10 +37,9 @@ import TabPanel from "primevue/tabpanel"; import Api from "../../api"; import ApiWithToast from "../../api-toast"; import { useLoadingStore } from "../../stores/loading"; -import { useErrorStore } from "../../stores/errors"; const loadingStore = useLoadingStore(); -const errorStore = useErrorStore(); + const clientNames = ref([]); const client = ref({}); const { clientName } = defineProps({ @@ -50,10 +49,10 @@ const { clientName } = defineProps({ const getClientNames = async (type) => { loadingStore.setLoading(true); try { - const names = await Api.getCustomerNames(type); + const names = await Api.getClientNames(type); clientNames.value = names; } catch (error) { - errorStore.addError(error.message || "Error fetching client names"); + console.error("Error fetching client names in Client.vue: ", error.message || error); } finally { loadingStore.setLoading(false); } @@ -61,9 +60,14 @@ const getClientNames = async (type) => { const getClient = async (name) => { loadingStore.setLoading(true); - const clientData = await ApiWithToast.makeApiCall(() => Api.getClient(name)); - client.value = clientData || {}; - loadingStore.setLoading(false); + try { + const clientData = await Api.getClient(name); + client.value = clientData || {}; + } catch (error) { + console.error("Error fetching client data in Client.vue: ", error.message || error); + } finally { + loadingStore.setLoading(false); + } }; onMounted(async () => { diff --git a/frontend/src/components/pages/Clients.vue b/frontend/src/components/pages/Clients.vue index b28797c..bfb7faf 100644 --- a/frontend/src/components/pages/Clients.vue +++ b/frontend/src/components/pages/Clients.vue @@ -165,7 +165,7 @@ const tableActions = [ { label: "View Details", action: (rowData) => { - router.push(`/clients/${rowData.id}`); + router.push(`/clients/${rowData.customerName}`); }, type: "button", style: "info",