From 4c8e66d155d44e4a24aaef555ea1196a5caee0cd Mon Sep 17 00:00:00 2001 From: Casey Date: Fri, 9 Jan 2026 12:50:46 -0600 Subject: [PATCH 1/2] merge main --- custom_ui/api/db/addresses.py | 23 +- custom_ui/api/db/clients.py | 32 ++ custom_ui/api/db/contacts.py | 6 + custom_ui/api/db/estimates.py | 2 +- custom_ui/events/estimate.py | 47 +- custom_ui/events/sales_order.py | 15 +- custom_ui/install.py | 421 ++++++++++-------- frontend/src/components/clientView/TopBar.vue | 4 +- frontend/src/stores/theme.js | 5 + 9 files changed, 349 insertions(+), 206 deletions(-) diff --git a/custom_ui/api/db/addresses.py b/custom_ui/api/db/addresses.py index 055ea3c..5a869ed 100644 --- a/custom_ui/api/db/addresses.py +++ b/custom_ui/api/db/addresses.py @@ -1,4 +1,5 @@ import frappe +import json from custom_ui.db_utils import build_error_response, build_success_response @frappe.whitelist() @@ -54,10 +55,8 @@ def get_contacts_for_address(address_name): def get_addresses(fields=["*"], filters={}): """Get addresses with optional filtering.""" if isinstance(fields, str): - import json fields = json.loads(fields) if isinstance(filters, str): - import json filters = json.loads(filters) if fields[0] != "*" and len(fields) == 1: pluck = fields[0] @@ -86,6 +85,16 @@ def create_address(address_data): address.insert(ignore_permissions=True) return address +def update_address(address_data): + """Update an existing address.""" + if isinstance(address_data, str): + address_data = json.loads(address_data) + address_doc = check_and_get_address_by_name(address_data.get("name")) + for key, value in address_data.items(): + setattr(address_doc, key, value) + address_doc.save(ignore_permissions=True) + return address_doc + def address_exists(address_line1, address_line2, city, state, pincode): """Check if an address with the given details already exists.""" filters = { @@ -97,6 +106,16 @@ def address_exists(address_line1, address_line2, city, state, pincode): } return frappe.db.exists("Address", filters) is not None +def check_and_get_address_by_name(address_name): + """Check if an address exists by name and return the address document if found.""" + if frappe.db.exists("Address", address_name): + return frappe.get_doc("Address", address_name) + raise ValueError(f"Address with name {address_name} does not exist.") + +def address_exists_by_name(address_name): + """Check if an address with the given name exists.""" + return frappe.db.exists("Address", address_name) is not None + def calculate_address_title(customer_name, address_data): return f"{customer_name} - {address_data.get('address_line1', '')}, {address_data.get('city', '')} - {address_data.get('type', '')}" diff --git a/custom_ui/api/db/clients.py b/custom_ui/api/db/clients.py index f2e6e29..f8024f2 100644 --- a/custom_ui/api/db/clients.py +++ b/custom_ui/api/db/clients.py @@ -210,6 +210,38 @@ def get_clients_table_data(filters={}, sortings=[], page=1, page_size=10): except Exception as e: return build_error_response(str(e), 500) +@frappe.whitelist() +def update_client_info(client_name, data): + """Update client information for a given client.""" + try: + data = json.loads(data) + print("DEBUG: update_client_info called with client_name:", client_name, "and data:", data) + client_doc = check_and_get_client_doc(client_name) + if not client_doc: + return build_error_response(f"Client with name '{client_name}' does not exist.", 404) + address_updates = data.get("addresses", []) + contact_updates = data.get("contacts", []) + customer_updates = data.get("customer", {}) + # Update addresses + if address_updates: + for addr_data in address_updates: + update_address(addr_data) + # Update contacts + if contact_updates: + for contact_data in contact_updates: + update_contact(contact_data) + # Update customer/lead + if customer_updates: + for field, value in customer_updates.items(): + if hasattr(client_doc, field): + setattr(client_doc, field, value) + client_doc.save(ignore_permissions=True) + frappe.local.message_log = [] + return get_client(client_name) + 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): diff --git a/custom_ui/api/db/contacts.py b/custom_ui/api/db/contacts.py index 629caeb..d64b26f 100644 --- a/custom_ui/api/db/contacts.py +++ b/custom_ui/api/db/contacts.py @@ -24,6 +24,12 @@ def check_and_get_contact(first_name: str, last_name: str, email: str, phone: st return get_contact(contact_name) return None +def check_and_get_contact_by_name(contact_name: str): + """Check if a contact exists by name and return the contact document if found.""" + if frappe.db.exists("Contact", contact_name): + return get_contact(contact_name) + return None + def create_contact(contact_data: dict): """Create a new contact.""" contact = frappe.get_doc({ diff --git a/custom_ui/api/db/estimates.py b/custom_ui/api/db/estimates.py index cd8c64a..70983f7 100644 --- a/custom_ui/api/db/estimates.py +++ b/custom_ui/api/db/estimates.py @@ -419,7 +419,7 @@ def upsert_estimate(data): new_estimate = frappe.get_doc({ "doctype": "Quotation", "custom_requires_half_payment": data.get("requires_half_payment", 0), - "custom_installation_address": data.get("address_name"), + "custom_job_address": data.get("address_name"), "custom_current_status": "Draft", "contact_email": data.get("contact_email"), "party_name": data.get("customer"), diff --git a/custom_ui/events/estimate.py b/custom_ui/events/estimate.py index 7b398b4..fd61f06 100644 --- a/custom_ui/events/estimate.py +++ b/custom_ui/events/estimate.py @@ -1,37 +1,54 @@ import frappe from erpnext.selling.doctype.quotation.quotation import make_sales_order +from custom_ui.services import DbService def after_insert(doc, method): + print("DEBUG: after_insert hook triggered for Quotation:", doc.name) try: - print("DEBUG: after_insert hook triggered for Quotation:", doc.name) - if not doc.custom_installation_address: - print("ERROR: custom_installation_address is empty") - return - address_doc = frappe.get_doc("Address", doc.custom_installation_address) - address_doc.custom_estimate_sent_status = "In Progress" - address_doc.save() + template = doc.custom_project_template or "Other" + if template == "Other": + print("WARN: No project template specified.") + if template == "SNW Install": + print("DEBUG: SNW Install template detected, updating custom address field.") + DbService.set_value( + doctype="Address", + name=doc.custom_job_address, + fieldname="custom_estimate_sent_status", + value="Pending" + ) except Exception as e: print("ERROR in after_insert hook:", str(e)) frappe.log_error(f"Error in estimate after_insert: {str(e)}", "Estimate Hook Error") def after_save(doc, method): print("DEBUG: after_save hook triggered for Quotation:", doc.name) - if doc.custom_sent and doc.custom_response: + if doc.custom_sent and doc.custom_response and doc.custom_project_template == "SNW Install": print("DEBUG: Quotation has been sent, updating Address status") - address_doc = frappe.get_doc("Address", doc.custom_installation_address) - address_doc.custom_estimate_sent_status = "Completed" - address_doc.save() + try: + DbService.set_value( + doctype="Address", + name=doc.custom_job_address, + fieldname="custom_estimate_sent_status", + value="Sent" + ) + except Exception as e: + print("ERROR updating Address in after_save hook:", str(e)) + frappe.log_error(f"Error updating Address in estimate after_save: {str(e)}", "Estimate Hook Error") def on_update_after_submit(doc, method): print("DEBUG: on_update_after_submit hook triggered for Quotation:", doc.name) print("DEBUG: Current custom_current_status:", doc.custom_current_status) if doc.custom_current_status == "Estimate Accepted": doc.custom_current_status = "Won" - print("DEBUG: Creating Sales Order from accepted Estimate") - address_doc = frappe.get_doc("Address", doc.customer_address) - address_doc.custom_estimate_sent_status = "Completed" - address_doc.save() + if doc.custom_project_template == "SNW Install": + DbService.set_value( + doctype="Address", + name=doc.custom_job_address, + fieldname="custom_estimate_sent_status", + value="Completed" + ) try: + print("DEBUG: Creating Sales Order from accepted Estimate") new_sales_order = make_sales_order(doc.name) new_sales_order.custom_requires_half_payment = doc.requires_half_payment new_sales_order.custom_installation_address = doc.custom_installation_address diff --git a/custom_ui/events/sales_order.py b/custom_ui/events/sales_order.py index 1fff81d..1d35125 100644 --- a/custom_ui/events/sales_order.py +++ b/custom_ui/events/sales_order.py @@ -1,5 +1,5 @@ import frappe - +from custom_ui.services import DbService def on_submit(doc, method): print("DEBUG: Info from Sales Order") @@ -11,15 +11,18 @@ def on_submit(doc, method): try: print("Creating Project from Sales Order", doc.name) sales_order = frappe.get_doc("Sales Order", doc.name) - project_template = frappe.get_doc("Project Template", "SNW Install") + if not sales_order.custom_project_template: + return + project_template = DbService.get("Project Template", sales_order.custom_project_template) new_job = frappe.get_doc({ "doctype": "Project", - "custom_installation_address": sales_order.custom_installation_address, - "project_name": sales_order.custom_installation_address, - "project_template": project_template, + "custom_job_address": sales_order.custom_job_address, + "project_name": f"{sales_order.custom_project_template} - {sales_order.custom_job_address}", + "project_template": project_template.name, "custom_warranty_duration_days": 90, - "sales_order": sales_order + "sales_order": sales_order.name }) + # attatch the job to the sales_order links new_job.insert() frappe.db.commit() except Exception as e: diff --git a/custom_ui/install.py b/custom_ui/install.py index c5b2af3..c7b000f 100644 --- a/custom_ui/install.py +++ b/custom_ui/install.py @@ -1,3 +1,4 @@ +from curses import meta import os import subprocess import sys @@ -23,14 +24,14 @@ def after_migrate(): update_onsite_meeting_fields() frappe.db.commit() - # Proper way to refresh metadata - frappe.clear_cache(doctype="Address") - frappe.reload_doctype("Address") - frappe.clear_cache(doctype="On-Site Meeting") - frappe.reload_doctype("On-Site Meeting") + # Proper way to refresh metadata for all doctypes with custom fields + doctypes_to_refresh = ["Lead", "Address", "Contact", "On-Site Meeting", "Quotation", "Sales Order", "Project Template"] + for doctype in doctypes_to_refresh: + frappe.clear_cache(doctype=doctype) + frappe.reload_doctype(doctype) - # update_address_fields() - build_frontend() + update_address_fields() + # build_frontend() def build_frontend(): @@ -68,7 +69,7 @@ def build_frontend(): def add_custom_fields(): from frappe.custom.doctype.custom_field.custom_field import create_custom_fields - print("\nšŸ”§ Adding custom fields to Address doctype...") + print("\nšŸ”§ Adding custom fields to doctypes...") custom_fields = { "Lead": [ @@ -208,7 +209,17 @@ def add_custom_fields(): fieldtype="Link", options="Project Template", insert_after="custom_quotation_template", - description="The project template to use when creating a project from this quotation." + description="The project template to use when creating a project from this quotation.", + allow_on_submit=1 + ), + dict( + fieldname="custom_job_address", + label="Job Address", + fieldtype="Link", + options="Address", + insert_after="custom_installation_address", + description="The address where the job will be performed.", + allow_on_submit=1 ) ], "Sales Order": [ @@ -225,7 +236,17 @@ def add_custom_fields(): fieldtype="Link", options="Project Template", description="The project template to use when creating a project from this sales order.", - insert_after="custom_installation_address" + insert_after="custom_installation_address", + allow_on_submit=1 + ), + dict( + fieldname="custom_job_address", + label="Job Address", + fieldtype="Link", + options="Address", + insert_after="custom_installation_address", + description="The address where the job will be performed.", + allow_on_submit=1 ) ], "Project Template": [ @@ -240,32 +261,68 @@ def add_custom_fields(): ] } - lead_field_count = len(custom_fields["Lead"]) - address_field_count = len(custom_fields["Address"]) - contact_field_count = len(custom_fields["Contact"]) - onsite_field_count = len(custom_fields["On-Site Meeting"]) - quotation_field_count = len(custom_fields["Quotation"]) - sales_order_field_count = len(custom_fields["Sales Order"]) - project_template_field_count = len(custom_fields["Project Template"]) - field_count = (lead_field_count + address_field_count + contact_field_count + - onsite_field_count + quotation_field_count + - sales_order_field_count + project_template_field_count) - print(f"šŸ”§ Preparing to add {field_count} custom fields:") - print(f" • Lead: {lead_field_count} fields") - print(f" • Address: {address_field_count} fields") - print(f" • Contact: {contact_field_count} fields") - print(f" • On-Site Meeting: {onsite_field_count} fields") - print(f" • Quotation: {quotation_field_count} fields") - print(f" • Sales Order: {sales_order_field_count} fields") - print(f" • Project Template: {project_template_field_count} fields") + print("šŸ”§ Custom fields to check per doctype:") + for key, value in custom_fields.items(): + print(f" • {key}: {len(value)} fields") + print(f" Total fields to check: {sum(len(v) for v in custom_fields.values())}\n") + + missing_fields = [] + fields_to_update = [] + + for doctype, field_options in custom_fields.items(): + meta = frappe.get_meta(doctype) + for field_spec in field_options: + fieldname = field_spec["fieldname"] + if not meta.has_field(fieldname): + missing_fields.append(f"{doctype}: {fieldname}") + else: + # Field exists, check if specs match + custom_field_name = f"{doctype}-{fieldname}" + if frappe.db.exists("Custom Field", custom_field_name): + custom_field_doc = frappe.get_doc("Custom Field", custom_field_name) + needs_update = False + + # Compare important properties + for key, desired_value in field_spec.items(): + if key == "fieldname": + continue + current_value = getattr(custom_field_doc, key, None) + if current_value != desired_value: + needs_update = True + break + + if needs_update: + fields_to_update.append((doctype, fieldname, field_spec)) + + if missing_fields: + print("\nāŒ Missing custom fields:") + for entry in missing_fields: + print(f" • {entry}") + print("\nšŸ”§ Creating missing custom fields...") + missing_field_specs = build_missing_field_specs(custom_fields, missing_fields) + create_custom_fields(missing_field_specs) + print("āœ… Missing custom fields created.") + + if fields_to_update: + print("\nšŸ”§ Updating custom fields with mismatched specs:") + for doctype, fieldname, field_spec in fields_to_update: + print(f" • {doctype}: {fieldname}") + custom_field_name = f"{doctype}-{fieldname}" + custom_field_doc = frappe.get_doc("Custom Field", custom_field_name) + + # Update all properties from field_spec + for key, value in field_spec.items(): + if key != "fieldname": + setattr(custom_field_doc, key, value) + + custom_field_doc.save(ignore_permissions=True) + + frappe.db.commit() + print("āœ… Custom fields updated.") + + if not missing_fields and not fields_to_update: + print("āœ… All custom fields verified.") - try: - create_custom_fields(custom_fields) - print("āœ… Custom fields added successfully!") - except Exception as e: - print(f"āŒ Error creating custom fields: {str(e)}") - frappe.log_error(message=str(e), title="Custom Fields Creation Failed") - raise def update_onsite_meeting_fields(): """Update On-Site Meeting doctype fields to make start_time and end_time optional.""" @@ -297,90 +354,55 @@ def update_onsite_meeting_fields(): # Don't raise - this is not critical enough to stop migration def update_address_fields(): + quotations = frappe.get_all("Quotation", pluck="name") addresses = frappe.get_all("Address", pluck="name") + sales_orders = frappe.get_all("Sales Order", pluck="name") total_addresses = len(addresses) + total_quotations = len(quotations) + total_sales_orders = len(sales_orders) + total_doctypes = total_addresses + total_quotations + total_sales_orders + combined_doctypes = [] + for sales_order in sales_orders: + combined_doctypes.append({"doctype": "Sales Order", "name": sales_order}) + for quotation in quotations: + combined_doctypes.append({"doctype": "Quotation", "name": quotation}) + for address in addresses: + combined_doctypes.append({"doctype": "Address", "name": address}) + - if total_addresses == 0: - print("šŸ“ No addresses found to update.") - return - - print(f"\nšŸ“ Updating fields for {total_addresses} addresses...") - - # Verify custom fields exist by checking the meta for every doctype that was customized - def has_any_field(meta, candidates): - return any(meta.has_field(f) for f in candidates) - - custom_field_expectations = { - "Address": [ - ["full_address"], - ["custom_onsite_meeting_scheduled", "onsite_meeting_scheduled"], - ["custom_estimate_sent_status", "estimate_sent_status"], - ["custom_job_status", "job_status"], - ["custom_payment_received_status", "payment_received_status",], - ["custom_lead_name", "lead_name"] - ], - "Contact": [ - ["custom_role", "role"], - ["custom_email", "email"], - ], - "On-Site Meeting": [ - ["custom_notes", "notes"], - ["custom_assigned_employee", "assigned_employee"], - ["custom_status", "status"], - ["custom_completed_by", "completed_by"] - ], - "Quotation": [ - ["custom_requires_half_payment", "requires_half_payment"] - ], - "Sales Order": [ - ["custom_requires_half_payment", "requires_half_payment"] - ], - "Lead": [ - ["custom_customer_type", "customer_type"] - ] - } - - missing_fields = [] - for doctype, field_options in custom_field_expectations.items(): - meta = frappe.get_meta(doctype) - for candidates in field_options: - if not has_any_field(meta, candidates): - missing_fields.append(f"{doctype}: {'/'.join(candidates)}") - - if missing_fields: - print("\nāŒ Missing custom fields:") - for entry in missing_fields: - print(f" • {entry}") - print(" Custom fields creation may have failed. Skipping address updates.") - return - - print("āœ… All custom fields verified. Proceeding with address updates...") + print(f"\nšŸ“ Updating field values for {total_addresses} addresses, {total_quotations} quotations, and {total_sales_orders} sales orders...") # Field update counters field_counters = { + 'quotation_addresses_updated': 0, + 'quotation_project_templates_updated': 0, + 'sales_order_addresses_updated': 0, + 'sales_order_project_templates_updated': 0, 'full_address': 0, 'custom_onsite_meeting_scheduled': 0, 'custom_estimate_sent_status': 0, 'custom_job_status': 0, - 'custom_payment_received_status': 0 + 'custom_payment_received_status': 0, + 'total_field_updates': 0, + 'addresses_updated': 0, + 'quotations_updated': 0, + 'sales_orders_updated': 0 } - total_field_updates = 0 - addresses_updated = 0 onsite_meta = frappe.get_meta("On-Site Meeting") onsite_status_field = "custom_status" if onsite_meta.has_field("custom_status") else "status" - for index, name in enumerate(addresses, 1): + for index, doc in enumerate(combined_doctypes, 1): # Calculate progress - progress_percentage = int((index / total_addresses) * 100) + progress_percentage = int((index / total_doctypes) * 100) bar_length = 30 - filled_length = int(bar_length * index // total_addresses) + filled_length = int(bar_length * index // total_doctypes) bar = 'ā–ˆ' * filled_length + 'ā–‘' * (bar_length - filled_length) # Print a three-line, refreshing progress block without adding new lines each loop - progress_line = f"šŸ“Š Progress: [{bar}] {progress_percentage:3d}% ({index}/{total_addresses})" - counters_line = f" Fields updated: {total_field_updates} | Addresses updated: {addresses_updated}" - detail_line = f" Processing: {name[:40]}..." + progress_line = f"šŸ“Š Progress: [{bar}] {progress_percentage:3d}% ({index}/{total_doctypes})" + counters_line = f" Fields updated: {field_counters['total_field_updates']} | DocTypes updated: {field_counters['addresses_updated'] + field_counters['quotations_updated'] + field_counters['sales_orders_updated']}" + detail_line = f" Processing: {doc['name'][:40]}..." if index == 1: # First render: write the three lines @@ -396,100 +418,139 @@ def update_address_fields(): sys.stdout.write(f"\033[K{counters_line}\n") sys.stdout.write(f"\033[K{detail_line}") - if index == total_addresses: + if index == total_doctypes: sys.stdout.write("\n") sys.stdout.flush() - should_update = False - address = frappe.get_doc("Address", name) - current_address_updates = 0 - - # Use getattr with default values instead of direct attribute access - if not getattr(address, 'full_address', None): - address_parts_1 = [ - address.address_line1 or "", - address.address_line2 or "", - address.city or "", - ] - address_parts_2 = [ - address.state or "", - address.pincode or "", - ] + if doc['doctype'] == "Quotation" or doc['doctype'] == "Sales Order": + dict_field = doc['doctype'].lower().replace(" ", "_") + quotation_doc = frappe.get_doc(doc['doctype'], doc['name']) + custom_installation_address = getattr(quotation_doc, 'custom_installation_address', None) + custom_job_address = getattr(quotation_doc, 'custom_job_address', None) + custom_project_template = getattr(quotation_doc, 'custom_project_template', None) - full_address = ", ".join([ - " ".join(filter(None, address_parts_1)), - " ".join(filter(None, address_parts_2)) - ]).strip() - address.full_address = full_address - field_counters['full_address'] += 1 - current_address_updates += 1 - should_update = True - onsite_meeting = "Not Started" - estimate_sent = "Not Started" - job_status = "Not Started" - payment_received = "Not Started" - - onsite_meetings = frappe.get_all("On-Site Meeting", fields=[onsite_status_field], filters={"address": address.address_title}) - if onsite_meetings and onsite_meetings[0]: - status_value = onsite_meetings[0].get(onsite_status_field) - onsite_meeting = "Completed" if status_value == "Completed" else "In Progress" - - estimates = frappe.get_all("Quotation", fields=["custom_sent", "docstatus", "custom_response"], filters={"custom_installation_address": address.address_title}) - if estimates and estimates[0] and estimates[0]["custom_sent"] == 1 and estimates[0]["custom_response"]: - estimate_sent = "Completed" - elif estimates and estimates[0] and not (estimates[0]["custom_sent"] == 1 and estimates[0]["custom_response"]): - estimate_sent = "In Progress" + updates = {} + if custom_installation_address and not custom_job_address: + updates['custom_job_address'] = custom_installation_address + field_counters[f"{dict_field}_addresses_updated"] += 1 + field_counters['total_field_updates'] += 1 + if custom_installation_address and not custom_project_template: + updates['custom_project_template'] = "SNW Install" + field_counters[f"{dict_field}_project_templates_updated"] += 1 + field_counters['total_field_updates'] += 1 - jobs = frappe.get_all("Project", fields=["status"], filters={"custom_installation_address": address.address_title, "project_template": "SNW Install"}) - if jobs and jobs[0] and jobs[0]["status"] == "Completed": - job_status = "Completed" - elif jobs and jobs[0]: - job_status = "In Progress" - - sales_invoices = frappe.get_all("Sales Invoice", fields=["outstanding_amount"], filters={"custom_installation_address": address.address_title}) - # payments = frappe.get_all("Payment Entry", filters={"custom_installation_address": address.address_title}) - if sales_invoices and sales_invoices[0] and sales_invoices[0]["outstanding_amount"] == 0: - payment_received = "Completed" - elif sales_invoices and sales_invoices[0]: - payment_received = "In Progress" - - if getattr(address, 'custom_onsite_meeting_scheduled', None) != onsite_meeting: - address.custom_onsite_meeting_scheduled = onsite_meeting - field_counters['custom_onsite_meeting_scheduled'] += 1 - current_address_updates += 1 - should_update = True - if getattr(address, 'custom_estimate_sent_status', None) != estimate_sent: - address.custom_estimate_sent_status = estimate_sent - field_counters['custom_estimate_sent_status'] += 1 - current_address_updates += 1 - should_update = True - if getattr(address, 'custom_job_status', None) != job_status: - address.custom_job_status = job_status - field_counters['custom_job_status'] += 1 - current_address_updates += 1 - should_update = True - if getattr(address, 'custom_payment_received_status', None) != payment_received: - address.custom_payment_received_status = payment_received - field_counters['custom_payment_received_status'] += 1 - current_address_updates += 1 - should_update = True + if updates: + frappe.db.set_value(doc['doctype'], doc['name'], updates) + field_counters[f"{dict_field}s_updated"] += 1 + + if doc['doctype'] == "Address": + address_doc = frappe.get_doc("Address", doc['name']) + updates = {} - if should_update: - address.save(ignore_permissions=True) - addresses_updated += 1 - total_field_updates += current_address_updates + # Use getattr with default values instead of direct attribute access + if not getattr(address_doc, 'full_address', None): + address_parts_1 = [ + address_doc.address_line1 or "", + address_doc.address_line2 or "", + address_doc.city or "", + ] + address_parts_2 = [ + address_doc.state or "", + address_doc.pincode or "", + ] + + full_address = ", ".join([ + " ".join(filter(None, address_parts_1)), + " ".join(filter(None, address_parts_2)) + ]).strip() + updates['full_address'] = full_address + field_counters['full_address'] += 1 + field_counters['total_field_updates'] += 1 + + onsite_meeting = "Not Started" + estimate_sent = "Not Started" + job_status = "Not Started" + payment_received = "Not Started" + + + onsite_meetings = frappe.get_all("On-Site Meeting", fields=[onsite_status_field], filters={"address": address_doc.address_title}) + if onsite_meetings and onsite_meetings[0]: + status_value = onsite_meetings[0].get(onsite_status_field) + onsite_meeting = "Completed" if status_value == "Completed" else "In Progress" + + estimates = frappe.get_all("Quotation", fields=["custom_sent", "docstatus", "custom_response"], filters={"custom_job_address": address_doc.address_title}) + if estimates and estimates[0] and estimates[0]["custom_sent"] == 1 and estimates[0]["custom_response"]: + estimate_sent = "Completed" + elif estimates and estimates[0] and not (estimates[0]["custom_sent"] == 1 and estimates[0]["custom_response"]): + estimate_sent = "In Progress" + + jobs = frappe.get_all("Project", fields=["status"], filters={"custom_installation_address": address_doc.address_title, "project_template": "SNW Install"}) + if jobs and jobs[0] and jobs[0]["status"] == "Completed": + job_status = "Completed" + elif jobs and jobs[0]: + job_status = "In Progress" + + sales_invoices = frappe.get_all("Sales Invoice", fields=["outstanding_amount"], filters={"custom_installation_address": address_doc.address_title}) + # payments = frappe.get_all("Payment Entry", filters={"custom_installation_address": address_doc.address_title}) + if sales_invoices and sales_invoices[0] and sales_invoices[0]["outstanding_amount"] == 0: + payment_received = "Completed" + elif sales_invoices and sales_invoices[0]: + payment_received = "In Progress" + + if getattr(address_doc, 'custom_onsite_meeting_scheduled', None) != onsite_meeting: + updates['custom_onsite_meeting_scheduled'] = onsite_meeting + field_counters['custom_onsite_meeting_scheduled'] += 1 + field_counters['total_field_updates'] += 1 + if getattr(address_doc, 'custom_estimate_sent_status', None) != estimate_sent: + updates['custom_estimate_sent_status'] = estimate_sent + field_counters['custom_estimate_sent_status'] += 1 + field_counters['total_field_updates'] += 1 + if getattr(address_doc, 'custom_job_status', None) != job_status: + updates['custom_job_status'] = job_status + field_counters['custom_job_status'] += 1 + field_counters['total_field_updates'] += 1 + if getattr(address_doc, 'custom_payment_received_status', None) != payment_received: + updates['custom_payment_received_status'] = payment_received + field_counters['custom_payment_received_status'] += 1 + field_counters['total_field_updates'] += 1 + + if updates: + frappe.db.set_value("Address", doc['name'], updates) + field_counters['addresses_updated'] += 1 + + # Commit every 100 records to avoid long transactions + if index % 100 == 0: + frappe.db.commit() # Print completion summary - print(f"\n\nāœ… Address field update completed!") + print(f"\n\nāœ… DocType field value update completed!") print(f"šŸ“Š Summary:") - print(f" • Total addresses processed: {total_addresses:,}") - print(f" • Addresses updated: {addresses_updated:,}") - print(f" • Total field updates: {total_field_updates:,}") + print(f" • Total DocTypes processed: {total_doctypes:,}") + print(f" • Addresses updated: {field_counters['addresses_updated']:,}") + print(f" • Quotations updated: {field_counters['quotations_updated']:,}") + print(f" • Sales Orders updated: {field_counters['sales_orders_updated']:,}") + print(f" • Total field updates: {field_counters['total_field_updates']:,}") print(f"\nšŸ“ Field-specific updates:") print(f" • Full Address: {field_counters['full_address']:,}") print(f" • On-Site Meeting Status: {field_counters['custom_onsite_meeting_scheduled']:,}") print(f" • Estimate Sent Status: {field_counters['custom_estimate_sent_status']:,}") print(f" • Job Status: {field_counters['custom_job_status']:,}") print(f" • Payment Received Status: {field_counters['custom_payment_received_status']:,}") - print("šŸ“ Address field updates complete.\n") \ No newline at end of file + print(f" • Quotation Addresses Updated: {field_counters['quotation_addresses_updated']:,}") + print(f" • Quotation Project Templates Updated: {field_counters['quotation_project_templates_updated']:,}") + print(f" • Sales Order Addresses Updated: {field_counters['sales_order_addresses_updated']:,}") + print(f" • Sales Order Project Templates Updated: {field_counters['sales_order_project_templates_updated']:,}") + print("šŸ“ DocType field value updates complete.\n") + +def build_missing_field_specs(custom_fields, missing_fields): + missing_field_specs = {} + for entry in missing_fields: + doctype, fieldname = entry.split(": ") + missing_field_specs.setdefault(doctype, []) + for field_spec in custom_fields.get(doctype, []): + if field_spec["fieldname"] == fieldname: + missing_field_specs[doctype].append(field_spec) + break + + return missing_field_specs \ No newline at end of file diff --git a/frontend/src/components/clientView/TopBar.vue b/frontend/src/components/clientView/TopBar.vue index 906f4e7..556bb04 100644 --- a/frontend/src/components/clientView/TopBar.vue +++ b/frontend/src/components/clientView/TopBar.vue @@ -93,7 +93,7 @@ const formatDate = (date) => { .section-label { font-weight: 500; - color: var(--theme-text-muted); + color: var(--theme-text-dark); white-space: nowrap; font-size: 0.85rem; } @@ -130,7 +130,7 @@ const formatDate = (date) => { .customer-name { font-size: 0.75rem; - color: var(--theme-text-muted); + color: var(--theme-text-dark); font-weight: 500; } diff --git a/frontend/src/stores/theme.js b/frontend/src/stores/theme.js index 16ddcc9..c451883 100644 --- a/frontend/src/stores/theme.js +++ b/frontend/src/stores/theme.js @@ -21,6 +21,7 @@ const themeMap = { textMuted: "#8798afff", textDark: "#0b1220", textLight: "#ffffff", + textAlt: "#06274aff", }, "Nuco Yard Care": { primary: "#3b7f2f", @@ -42,6 +43,7 @@ const themeMap = { textMuted: "#4b5b35", textDark: "#192016", textLight: "#ffffff", + textAlt: "#172519ff", }, "Lowe Fencing": { primary: "#2f3b52", @@ -63,6 +65,7 @@ const themeMap = { textMuted: "#42506a", textDark: "#0b1220", textLight: "#ffffff", + textAlt: "#291d3aff", }, "Veritas Stone": { primary: "#7a6f63", @@ -84,6 +87,7 @@ const themeMap = { textMuted: "#5a5047", textDark: "#231c16", textLight: "#ffffff", + textAlt: "#25201bff", }, "Daniels Landscape Supplies": { primary: "#2f6b2f", @@ -105,6 +109,7 @@ const themeMap = { textMuted: "#4f5b3f", textDark: "#162016", textLight: "#ffffff", + textAlt: "#172519ff", }, }; From 2f1c975e0ae8d6387a6e448755a689f57330d198 Mon Sep 17 00:00:00 2001 From: Casey Date: Fri, 9 Jan 2026 12:51:25 -0600 Subject: [PATCH 2/2] merge main --- custom_ui/services/__init__.py | 3 + custom_ui/services/address_service.py | 81 +++++++++++++++++++++++ custom_ui/services/db_service.py | 91 ++++++++++++++++++++++++++ custom_ui/services/estimate_service.py | 87 ++++++++++++++++++++++++ 4 files changed, 262 insertions(+) create mode 100644 custom_ui/services/__init__.py create mode 100644 custom_ui/services/address_service.py create mode 100644 custom_ui/services/db_service.py create mode 100644 custom_ui/services/estimate_service.py diff --git a/custom_ui/services/__init__.py b/custom_ui/services/__init__.py new file mode 100644 index 0000000..959e02b --- /dev/null +++ b/custom_ui/services/__init__.py @@ -0,0 +1,3 @@ +from .address_service import AddressService + +from .db_service import DbService \ No newline at end of file diff --git a/custom_ui/services/address_service.py b/custom_ui/services/address_service.py new file mode 100644 index 0000000..4ee3eca --- /dev/null +++ b/custom_ui/services/address_service.py @@ -0,0 +1,81 @@ +import frappe + +class AddressService: + + @staticmethod + def exists(address_name: str) -> bool: + """Check if an address with the given name exists.""" + print(f"DEBUG: Checking existence of Address with name: {address_name}") + result = frappe.db.exists("Address", address_name) is not None + print(f"DEBUG: Address existence: {result}") + return result + + @staticmethod + def get(address_name: str): + """Retrieve an address document by name. Returns None if not found.""" + print(f"DEBUG: Retrieving Address document with name: {address_name}") + if AddressService.exists(address_name): + address_doc = frappe.get_doc("Address", address_name) + print("DEBUG: Address document found.") + return address_doc + print("DEBUG: Address document not found.") + return None + + @staticmethod + def get_or_throw(address_name: str): + """Retrieve an address document by name or throw an error if not found.""" + address_doc = AddressService.get(address_name) + if address_doc: + return address_doc + raise ValueError(f"Address with name {address_name} does not exist.") + + @staticmethod + def update_value(docname: str, fieldname: str, value, save: bool = True) -> frappe._dict: + """Update a specific field value of a document.""" + print(f"DEBUG: Updating Address {docname}, setting {fieldname} to {value}") + if AddressService.exists(docname) is False: + raise ValueError(f"Address with name {docname} does not exist.") + if save: + print("DEBUG: Saving updated Address document.") + address_doc = AddressService.get_or_throw(docname) + setattr(address_doc, fieldname, value) + address_doc.save(ignore_permissions=True) + else: + print("DEBUG: Not saving Address document as save=False.") + frappe.db.set_value("Address", docname, fieldname, value) + print(f"DEBUG: Updated Address {docname}: set {fieldname} to {value}") + return address_doc + + @staticmethod + def get_value(docname: str, fieldname: str): + """Get a specific field value of a document. Returns None if document does not exist.""" + print(f"DEBUG: Getting value of field {fieldname} from Address {docname}") + if not AddressService.exists(docname): + print("DEBUG: Value cannot be retrieved; Address does not exist.") + return None + value = frappe.db.get_value("Address", docname, fieldname) + print(f"DEBUG: Retrieved value: {value}") + return value + + @staticmethod + def get_value_or_throw(docname: str, fieldname: str): + """Get a specific field value of a document or throw an error if document does not exist.""" + value = AddressService.get_value(docname, fieldname) + if value is not None: + return value + raise ValueError(f"Address with name {docname} does not exist.") + + @staticmethod + def create(address_data: dict): + """Create a new address.""" + print("DEBUG: Creating new Address with data:", address_data) + address = frappe.get_doc({ + "doctype": "Address", + **address_data + }) + address.insert(ignore_permissions=True) + print("DEBUG: Created new Address:", address.as_dict()) + return address + + + \ No newline at end of file diff --git a/custom_ui/services/db_service.py b/custom_ui/services/db_service.py new file mode 100644 index 0000000..6035bdb --- /dev/null +++ b/custom_ui/services/db_service.py @@ -0,0 +1,91 @@ +import frappe + +class DbService: + + @staticmethod + def exists(doctype: str, name: str) -> bool: + """Check if a document exists by doctype and name.""" + result = frappe.db.exists(doctype, name) is not None + print(f"DEBUG: {doctype} existence for {name}: {result}") + return result + + @staticmethod + def get(doctype: str, name: str): + """Retrieve a document by doctype and name. Returns None if not found.""" + print(f"DEBUG: Retrieving {doctype} document with name: {name}") + if DbService.exists(doctype, name): + doc = frappe.get_doc(doctype, name) + print(f"DEBUG: {doctype} document found.") + return doc + print(f"DEBUG: {doctype} document not found.") + return None + + @staticmethod + def get_or_throw(doctype: str, name: str): + """Retrieve a document by doctype and name or throw an error if not found.""" + doc = DbService.get(doctype, name) + if doc: + return doc + raise ValueError(f"{doctype} document with name {name} does not exist.") + + @staticmethod + def get_value(doctype: str, name: str, fieldname: str): + """Get a specific field value of a document. Returns None if document does not exist.""" + print(f"DEBUG: Getting value of field {fieldname} from {doctype} {name}") + if not DbService.exists(doctype, name): + print("DEBUG: Value cannot be retrieved; document does not exist.") + return None + value = frappe.db.get_value(doctype, name, fieldname) + print(f"DEBUG: Retrieved value: {value}") + return value + + @staticmethod + def get_value_or_throw(doctype: str, name: str, fieldname: str): + """Get a specific field value of a document or throw an error if document does not exist.""" + value = DbService.get_value(doctype, name, fieldname) + if value is not None: + return value + raise ValueError(f"{doctype} document with name {name} does not exist.") + + @staticmethod + def create(doctype: str, data: dict): + """Create a new document of the specified doctype.""" + print(f"DEBUG: Creating new {doctype} document with data: {data}") + doc = frappe.get_doc({ + "doctype": doctype, + **data + }) + doc.insert(ignore_permissions=True) + print(f"DEBUG: Created new {doctype} document with name: {doc.name}") + return doc + + @staticmethod + def set_value(doctype: str, name: str, fieldname: str, value: any, save: bool = True): + """Set a specific field value of a document.""" + print(f"DEBUG: Setting value of field {fieldname} in {doctype} {name} to {value}") + if save: + print("DEBUG: Saving updated document.") + doc = DbService.get_or_throw(doctype, name) + setattr(doc, fieldname, value) + doc.save(ignore_permissions=True) + return doc + else: + print("DEBUG: Not saving document as save=False.") + frappe.db.set_value(doctype, name, fieldname, value) + return None + + @staticmethod + def update(doctype: str, name: str, update_data: dict, save: bool = True): + """Update an existing document of the specified doctype.""" + print(f"DEBUG: Updating {doctype} {name}") + doc = DbService.get_or_throw(doctype, name) + for key, value in update_data.items(): + setattr(doc, key, value) + if save: + doc.save(ignore_permissions=True) + else: + DbService.set_value(doctype, name, key, value, save=False) + print(f"DEBUG: Updated {doctype} document: {doc.as_dict()}") + return doc + + \ No newline at end of file diff --git a/custom_ui/services/estimate_service.py b/custom_ui/services/estimate_service.py new file mode 100644 index 0000000..63a734e --- /dev/null +++ b/custom_ui/services/estimate_service.py @@ -0,0 +1,87 @@ +import frappe + +class EstimateService: + + @staticmethod + def exists(estimate_name: str) -> bool: + """Check if a Quotation document exists by name.""" + print(f"DEBUG: Checking existence of Quotation document with name: {estimate_name}") + result = frappe.db.exists("Quotation", estimate_name) is not None + print(f"DEBUG: Quotation document existence: {result}") + return result + + @staticmethod + def get(estimate_name: str) -> frappe._dict: + """Retrieve a Quotation document by name. Returns None if not found.""" + print(f"DEBUG: Retrieving Quotation document with name: {estimate_name}") + if EstimateService.exists(estimate_name): + estimate_doc = frappe.get_doc("Quotation", estimate_name) + print("DEBUG: Quotation document found.") + return estimate_doc + print("DEBUG: Quotation document not found.") + return None + + @staticmethod + def get_or_throw(estimate_name: str) -> frappe._dict: + """Retrieve a Quotation document by name or throw an error if not found.""" + estimate_doc = EstimateService.get(estimate_name) + if estimate_doc: + return estimate_doc + raise ValueError(f"Quotation document with name {estimate_name} does not exist.") + + @staticmethod + def update_value(docname: str, fieldname: str, value, save: bool = True) -> None: + """Update a specific field value of an Quotation document.""" + print(f"DEBUG: Updating Quotation {docname}, setting {fieldname} to {value}") + if save: + print("DEBUG: Saving updated Quotation document.") + estimate_doc = EstimateService.get_or_throw(docname) + setattr(estimate_doc, fieldname, value) + estimate_doc.save(ignore_permissions=True) + else: + print("DEBUG: Not saving Quotation document as save=False.") + frappe.db.set_value("Quotation", docname, fieldname, value) + print(f"DEBUG: Updated Quotation {docname}: set {fieldname} to {value}") + + @staticmethod + def get_value(docname: str, fieldname: str) -> any: + """Get a specific field value of a Quotation document. Returns None if document does not exist.""" + print(f"DEBUG: Getting value of field {fieldname} from Quotation {docname}") + if not EstimateService.exists(docname): + print("DEBUG: Value cannot be retrieved; Quotation document does not exist.") + return None + value = frappe.db.get_value("Quotation", docname, fieldname) + print(f"DEBUG: Retrieved value: {value}") + return value + + @staticmethod + def get_value_or_throw(docname: str, fieldname: str) -> any: + """Get a specific field value of a Quotation document or throw an error if document does not exist.""" + value = EstimateService.get_value(docname, fieldname) + if value is not None: + return value + raise ValueError(f"Quotation document with name {docname} does not exist.") + + @staticmethod + def update(estimate_name: str, update_data: dict) -> frappe._dict: + """Update an existing Quotation document.""" + print(f"DEBUG: Updating Quotation {estimate_name} with data: {update_data}") + estimate_doc = EstimateService.get_or_throw(estimate_name) + for key, value in update_data.items(): + setattr(estimate_doc, key, value) + estimate_doc.save(ignore_permissions=True) + print(f"DEBUG: Updated Quotation document: {estimate_doc.as_dict()}") + return estimate_doc + + @staticmethod + def create(estimate_data: dict) -> frappe._dict: + """Create a new Quotation document.""" + print(f"DEBUG: Creating new Quotation with data: {estimate_data}") + estimate_doc = frappe.get_doc({ + "doctype": "Quotation", + **estimate_data + }) + estimate_doc.insert(ignore_permissions=True) + print(f"DEBUG: Created Quotation document: {estimate_doc.as_dict()}") + return estimate_doc + \ No newline at end of file