add notifiaction handling, error handling

This commit is contained in:
Casey 2025-11-12 15:13:49 -06:00
parent ce708f5209
commit 1af288aa62
21 changed files with 4864 additions and 224 deletions

View file

@ -1,5 +1,5 @@
import frappe, json
from custom_ui.db_utils import process_query_conditions, build_datatable_response, get_count_or_filters
from custom_ui.db_utils import build_error_response, process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response
# ===============================================================================
# CLIENT MANAGEMENT API METHODS
@ -9,222 +9,257 @@ from custom_ui.db_utils import process_query_conditions, build_datatable_respons
def get_client_status_counts(weekly=False, week_start_date=None, week_end_date=None):
"""Get counts of clients by status categories with optional weekly filtering."""
# Build base filters for date range if weekly filtering is enabled
base_filters = {}
if weekly and week_start_date and week_end_date:
# Assuming you have a date field to filter by - adjust the field name as needed
# Common options: creation, modified, custom_date_field, etc.
base_filters["creation"] = ["between", [week_start_date, week_end_date]]
try:
base_filters = {}
if weekly and week_start_date and week_end_date:
# Assuming you have a date field to filter by - adjust the field name as needed
# Common options: creation, modified, custom_date_field, etc.
base_filters["creation"] = ["between", [week_start_date, week_end_date]]
# Helper function to merge base filters with status filters
def get_filters(status_field, status_value):
filters = {status_field: status_value}
filters.update(base_filters)
return filters
# Helper function to merge base filters with status filters
def get_filters(status_field, status_value):
filters = {status_field: status_value}
filters.update(base_filters)
return filters
onsite_meeting_scheduled_status_counts = {
"label": "On-Site Meeting Scheduled",
"not_started": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled", "Not Started")),
"in_progress": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled", "In Progress")),
"completed": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled", "Completed"))
}
estimate_sent_status_counts = {
"label": "Estimate Sent",
"not_started": frappe.db.count("Address", filters=get_filters("custom_estimate_sent_status", "Not Started")),
"in_progress": frappe.db.count("Address", filters=get_filters("custom_estimate_sent_status", "In Progress")),
"completed": frappe.db.count("Address", filters=get_filters("custom_estimate_sent_status", "Completed"))
}
job_status_counts = {
"label": "Job Status",
"not_started": frappe.db.count("Address", filters=get_filters("custom_job_status", "Not Started")),
"in_progress": frappe.db.count("Address", filters=get_filters("custom_job_status", "In Progress")),
"completed": frappe.db.count("Address", filters=get_filters("custom_job_status", "Completed"))
}
payment_received_status_counts = {
"label": "Payment Received",
"not_started": frappe.db.count("Address", filters=get_filters("custom_payment_received_status", "Not Started")),
"in_progress": frappe.db.count("Address", filters=get_filters("custom_payment_received_status", "In Progress")),
"completed": frappe.db.count("Address", filters=get_filters("custom_payment_received_status", "Completed"))
}
status_dicts = [
onsite_meeting_scheduled_status_counts,
estimate_sent_status_counts,
job_status_counts,
payment_received_status_counts
]
categories = []
for status_dict in status_dicts:
category = {
"label": status_dict["label"],
"statuses": [
{
"color": "red",
"label": "Not Started",
"count": status_dict["not_started"]
},
{
"color": "yellow",
"label": "In Progress",
"count": status_dict["in_progress"]
},
{
"color": "green",
"label": "Completed",
"count": status_dict["completed"]
}
]
onsite_meeting_scheduled_status_counts = {
"label": "On-Site Meeting Scheduled",
"not_started": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled", "Not Started")),
"in_progress": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled", "In Progress")),
"completed": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled", "Completed"))
}
categories.append(category)
return categories
estimate_sent_status_counts = {
"label": "Estimate Sent",
"not_started": frappe.db.count("Address", filters=get_filters("custom_estimate_sent_status", "Not Started")),
"in_progress": frappe.db.count("Address", filters=get_filters("custom_estimate_sent_status", "In Progress")),
"completed": frappe.db.count("Address", filters=get_filters("custom_estimate_sent_status", "Completed"))
}
job_status_counts = {
"label": "Job Status",
"not_started": frappe.db.count("Address", filters=get_filters("custom_job_status", "Not Started")),
"in_progress": frappe.db.count("Address", filters=get_filters("custom_job_status", "In Progress")),
"completed": frappe.db.count("Address", filters=get_filters("custom_job_status", "Completed"))
}
payment_received_status_counts = {
"label": "Payment Received",
"not_started": frappe.db.count("Address", filters=get_filters("custom_payment_received_status", "Not Started")),
"in_progress": frappe.db.count("Address", filters=get_filters("custom_payment_received_status", "In Progress")),
"completed": frappe.db.count("Address", filters=get_filters("custom_payment_received_status", "Completed"))
}
status_dicts = [
onsite_meeting_scheduled_status_counts,
estimate_sent_status_counts,
job_status_counts,
payment_received_status_counts
]
categories = []
for status_dict in status_dicts:
category = {
"label": status_dict["label"],
"statuses": [
{
"color": "red",
"label": "Not Started",
"count": status_dict["not_started"]
},
{
"color": "yellow",
"label": "In Progress",
"count": status_dict["in_progress"]
},
{
"color": "green",
"label": "Completed",
"count": status_dict["completed"]
}
]
}
categories.append(category)
return build_success_response(categories)
except frappe.ValidationError as ve:
return build_error_response(str(ve), 400)
except Exception as e:
return build_error_response(str(e), 500)
@frappe.whitelist()
def get_client(client_name):
"""Get detailed information for a specific client including address, customer, and projects."""
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
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 = []
onsite_meetings = []
quotations = []
sales_orders = []
projects = [frappe.get_doc("Project", proj["name"]) for proj in project_names]
sales_invoices = []
payment_entries = []
jobs = []
for project in projects:
job = []
jobs.append(job)
customer = frappe.get_doc("Customer", customer_name)
# get all associated data as needed
return {
"address": address,
"customer": customer,
"contacts": contacts,
"jobs": jobs,
"sales_invoices": sales_invoices,
"payment_entries": payment_entries,
"sales_orders": sales_orders,
"quotations": quotations,
"onsite_meetings": onsite_meetings,
}
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,
})
except frappe.ValidationError as ve:
return build_error_response(str(ve), 400)
except Exception as e:
return build_error_response(str(e), 500)
@frappe.whitelist()
def get_clients_table_data(filters={}, sortings=[], page=1, page_size=10):
"""Get paginated client table data with filtering and sorting support."""
print("DEBUG: Raw client table query received:", {
"filters": filters,
"sortings": sortings,
"page": page,
"page_size": page_size
})
processed_filters, processed_sortings, is_or, page, page_size = process_query_conditions(filters, sortings, page, page_size)
# Handle count with proper OR filter support
if is_or:
count = frappe.db.sql(*get_count_or_filters("Address", processed_filters))[0][0]
else:
count = frappe.db.count("Address", filters=processed_filters)
print("DEBUG: Count of addresses matching filters:", count)
address_names = frappe.db.get_all(
"Address",
fields=["name"],
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
)
addresses = [frappe.get_doc("Address", addr["name"]).as_dict() for addr in address_names]
tableRows = []
for address in addresses:
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"] = (
f"{address['address_line1']}"
f"{' ' + address['address_line2'] if address['address_line2'] else ''} "
f"{address['city']}, {address['state']} {address['pincode']}"
try:
print("DEBUG: Raw client table query received:", {
"filters": filters,
"sortings": sortings,
"page": page,
"page_size": page_size
})
processed_filters, processed_sortings, is_or, page, page_size = process_query_conditions(filters, sortings, page, page_size)
# Handle count with proper OR filter support
if is_or:
count = frappe.db.sql(*get_count_or_filters("Address", processed_filters))[0][0]
else:
count = frappe.db.count("Address", filters=processed_filters)
print("DEBUG: Count of addresses matching filters:", count)
address_names = frappe.db.get_all(
"Address",
fields=["name"],
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
)
tableRow["appointment_scheduled_status"] = address.custom_onsite_meeting_scheduled
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
tableRows.append(tableRow)
return build_datatable_response(data=tableRows, count=count, page=page, page_size=page_size)
addresses = [frappe.get_doc("Address", addr["name"]).as_dict() for addr in address_names]
tableRows = []
for address in addresses:
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"] = (
f"{address['address_line1']}"
f"{' ' + address['address_line2'] if address['address_line2'] else ''} "
f"{address['city']}, {address['state']} {address['pincode']}"
)
tableRow["appointment_scheduled_status"] = address.custom_onsite_meeting_scheduled
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
tableRows.append(tableRow)
tableDataDict = build_datatable_dict(data=tableRows, count=count, page=page, page_size=page_size)
return build_success_response(tableDataDict)
except frappe.ValidationError as ve:
return build_error_response(str(ve), 400)
except Exception as e:
return build_error_response(str(e), 500)
@frappe.whitelist()
def upsert_client(data):
"""Create or update a client (customer and address)."""
data = json.loads(data)
# Handle customer creation/update
customer = frappe.db.exists("Customer", {"customer_name": data.get("customer_name")})
if not customer:
customer_doc = frappe.get_doc({
"doctype": "Customer",
"customer_name": data.get("customer_name"),
"customer_type": data.get("customer_type")
try:
data = json.loads(data)
# Handle customer creation/update
customer = frappe.db.exists("Customer", {"customer_name": data.get("customer_name")})
if not customer:
customer_doc = frappe.get_doc({
"doctype": "Customer",
"customer_name": data.get("customer_name"),
"customer_type": data.get("customer_type")
}).insert(ignore_permissions=True)
else:
customer_doc = frappe.get_doc("Customer", customer)
print("Customer:", customer_doc.as_dict())
# Check for existing address
filters = {
"address_title": data.get("address_title"),
}
existing_address = frappe.db.exists("Address", filters)
print("Existing address check:", existing_address)
if existing_address:
frappe.throw(f"Address already exists for customer {data.get('customer_name')}.", frappe.ValidationError)
# Create address
address_doc = frappe.get_doc({
"doctype": "Address",
"address_line1": data.get("address_line1"),
"city": data.get("city"),
"state": data.get("state"),
"country": "United States",
"address_title": data.get("address_title"),
"pincode": data.get("pincode"),
"custom_customer_to_bill": customer_doc.name
}).insert(ignore_permissions=True)
else:
customer_doc = frappe.get_doc("Customer", customer)
print("Customer:", customer_doc.as_dict())
# Check for existing address
filters = {
"address_title": data.get("address_title"),
}
existing_address = frappe.db.exists("Address", filters)
print("Existing address check:", existing_address)
if existing_address:
frappe.throw(f"Address already exists for customer {data.get('customer_name')}.", frappe.ValidationError)
# Create address
address_doc = frappe.get_doc({
"doctype": "Address",
"address_line1": data.get("address_line1"),
"city": data.get("city"),
"state": data.get("state"),
"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 address to customer
link = {
"link_doctype": "Customer",
"link_name": customer_doc.name
}
address_doc.append("links", link)
address_doc.save(ignore_permissions=True)
# Link address to customer
link = {
"link_doctype": "Customer",
"link_name": customer_doc.name
}
address_doc.append("links", link)
address_doc.save(ignore_permissions=True)
return {
"customer": customer_doc,
"address": address_doc,
"success": True
}
return build_success_response({
"customer": customer_doc.as_dict(),
"address": address_doc.as_dict()
})
except frappe.ValidationError as ve:
return build_error_response(str(ve), 400)
except Exception as e:
return build_error_response(str(e), 500)

View file

@ -0,0 +1,34 @@
import frappe
from custom_ui.db_utils import build_success_response, build_error_response
# ===============================================================================
# CUSTOMER API METHODS
# ===============================================================================
@frappe.whitelist()
def get_customer_details(customer_name):
try:
customer = frappe.get_doc("Customer", customer_name)
return build_success_response(customer)
except frappe.ValidationError as ve:
return build_error_response(str(ve), 400)
except Exception as e:
return build_error_response(str(e), 500)
@frappe.whitelist()
def get_client_names(type):
"""Get a list of client names. Maps to value/label pairs for select fields."""
try:
customer_names = frappe.db.sql("""
SELECT
customer_name AS label,
name AS value
FROM
`tabCustomer`
WHERE
customer_type = %s
""", (type,), as_dict=True)
return build_success_response(customer_names)
except Exception as e:
return build_error_response(str(e), 500)
except frappe.ValidationError as ve:
return build_error_response(str(ve), 400)

View file

@ -29,6 +29,7 @@ def build_frontend(site):
click.echo("\n✅ Frontend build completed successfully.\n")
except subprocess.CalledProcessError as e:
click.echo(f"\n❌ Frontend build failed: {e}\n")
exit(1)
else:
frappe.log_error(message="No frontend directory found for custom_ui", title="Frontend Build Skipped")
click.echo(f"\n⚠️ Frontend directory does not exist. Skipping build. Path was {frontend_path}\n")

View file

@ -89,7 +89,7 @@ def process_query_conditions(filters, sortings, page, page_size):
return processed_filters, processed_sortings, is_or_filters, page_int, page_size_int
def build_datatable_response(data, count, page, page_size):
def build_datatable_dict(data, count, page, page_size):
return {
"pagination": {
"total": count,
@ -112,4 +112,17 @@ def get_count_or_filters(doctype, or_filters):
where_sql = " OR ".join(where_clauses)
sql = f"SELECT COUNT(*) FROM `tab{doctype}` WHERE {where_sql}"
return sql, values
def build_error_response(message, status_code=400):
return {
"status": "error",
"message": message,
"status_code": status_code
}
def build_success_response(data):
return {
"status": "success",
"data": data
}