From 8ed083fce1aea861a155545a82d14fdd057733fd Mon Sep 17 00:00:00 2001 From: Casey Date: Tue, 9 Dec 2025 16:38:58 -0600 Subject: [PATCH] lots of updates --- custom_ui/api/db/estimates.py | 42 ++++-- custom_ui/api/db/onsite_meetings.py | 10 +- custom_ui/events/estimate.py | 28 +++- custom_ui/events/onsite_meeting.py | 6 +- custom_ui/events/sales_order.py | 6 + custom_ui/hooks.py | 8 +- custom_ui/install.py | 99 ++++++++++---- custom_ui/templates/estimates/accepted.html | 86 ++++++++++++ custom_ui/templates/estimates/error.html | 74 ++++++++++ custom_ui/templates/estimates/rejected.html | 87 ++++++++++++ .../templates/estimates/request-call.html | 86 ++++++++++++ frontend/src/components/common/DataTable.vue | 37 ++++- frontend/src/components/pages/Estimate.vue | 128 +++++++++++++++--- .../src/components/pages/ScheduleOnSite.vue | 116 ++++++++++++++-- 14 files changed, 730 insertions(+), 83 deletions(-) create mode 100644 custom_ui/events/sales_order.py create mode 100644 custom_ui/templates/estimates/accepted.html create mode 100644 custom_ui/templates/estimates/error.html create mode 100644 custom_ui/templates/estimates/rejected.html create mode 100644 custom_ui/templates/estimates/request-call.html diff --git a/custom_ui/api/db/estimates.py b/custom_ui/api/db/estimates.py index 32294c9..714cb8f 100644 --- a/custom_ui/api/db/estimates.py +++ b/custom_ui/api/db/estimates.py @@ -1,6 +1,7 @@ import frappe, json from frappe.utils.pdf import get_pdf from custom_ui.db_utils import process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response +from werkzeug.wrappers import Response # =============================================================================== # ESTIMATES & INVOICES API METHODS @@ -152,7 +153,6 @@ def send_estimate_email(estimate_name): quotation.custom_sent = 1 quotation.save() updated_quotation = frappe.get_doc("Quotation", estimate_name) - print("DEBUG: Quotation submitted successfully.") return build_success_response(updated_quotation.as_dict()) except Exception as e: print(f"DEBUG: Error in send_estimate_email: {str(e)}") @@ -161,16 +161,35 @@ def send_estimate_email(estimate_name): @frappe.whitelist(allow_guest=True) def update_response(name, response): """Update the response for a given estimate.""" - estimate = frappe.get_doc("Quotation", name) - accepted = True if response == "Accepted" else False - new_status = "Estimate Accepted" if accepted else "Lost" - - estimate.custom_response = response - estimate.custom_current_status = new_status - estimate.custom_followup_needed = 1 if response == "Requested call" else 0 - estimate.flags.ignore_permissions = True - estimate.save() - frappe.db.commit() + print("DEBUG: RESPONSE RECEIVED:", name, response) + try: + if not frappe.db.exists("Quotation", name): + raise Exception("Estimate not found.") + estimate = frappe.get_doc("Quotation", name) + accepted = True if response == "Accepted" else False + new_status = "Estimate Accepted" if accepted else "Lost" + + estimate.custom_response = response + estimate.custom_current_status = new_status + estimate.custom_followup_needed = 1 if response == "Requested call" else 0 + estimate.flags.ignore_permissions = True + print("DEBUG: Updating estimate with response:", response, "and status:", new_status) + # estimate.save() + estimate.submit() + frappe.db.commit() + + if accepted: + template = "custom_ui/templates/estimates/accepted.html" + elif response == "Requested call": + template = "custom_ui/templates/estimates/request-call.html" + else: + template = "custom_ui/templates/estimates/rejected.html" + html = frappe.render_template(template, {"doc": estimate}) + return Response(html, mimetype="text/html") + except Exception as e: + template = "custom_ui/templates/estimates/error.html" + html = frappe.render_template(template, {"error_message": str(e)}) + return Response(html, mimetype="text/html") @@ -212,6 +231,7 @@ def upsert_estimate(data): print("DEBUG: Retrieved address name:", data.get("address_name")) 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_current_status": "Draft", "contact_email": data.get("contact_email"), diff --git a/custom_ui/api/db/onsite_meetings.py b/custom_ui/api/db/onsite_meetings.py index fddaf09..7ab4721 100644 --- a/custom_ui/api/db/onsite_meetings.py +++ b/custom_ui/api/db/onsite_meetings.py @@ -118,9 +118,16 @@ def update_onsite_meeting(name, data): try: if isinstance(data, str): data = json.loads(data) - data = {**defualts, **data} + + # Ensure we always have the expected keys so fields can be cleared + data = {**defualts, **(data or {})} meeting = frappe.get_doc("On-Site Meeting", name) for key, value in data.items(): + # Allow explicitly clearing date/time and assignment fields + if key in ["start_time", "end_time", "assigned_employee", "completed_by"] and value is None: + meeting.set(key, None) + continue + if value is not None: if key == "address": value = frappe.db.get_value("Address", {"full_address": value}, "name") @@ -128,6 +135,7 @@ def update_onsite_meeting(name, data): value = frappe.db.get_value("Employee", {"employee_name": value}, "name") meeting.set(key, value) meeting.save() + frappe.db.commit() return build_success_response(meeting.as_dict()) except frappe.DoesNotExistError: return build_error_response(f"On-Site Meeting '{name}' does not exist.", 404) diff --git a/custom_ui/events/estimate.py b/custom_ui/events/estimate.py index affb1dd..ae145f9 100644 --- a/custom_ui/events/estimate.py +++ b/custom_ui/events/estimate.py @@ -15,9 +15,25 @@ def after_insert(doc, method): frappe.log_error(f"Error in estimate after_insert: {str(e)}", "Estimate Hook Error") def after_save(doc, method): - if not doc.custom_sent or not doc.custom_response: - return - 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() + print("DEBUG: after_save hook triggered for Quotation:", doc.name) + if doc.custom_sent and doc.custom_response: + 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() + +def after_submit(doc, method): + print("DEBUG: on_submit hook triggered for Quotation:", doc.name) + if doc.custom_current_status == "Estimate Accepted": + print("DEBUG: Creating Sales Order from accepted Estimate") + address_doc = frappe.get_doc("Address", doc.custom_installation_address) + address_doc.custom_estimate_sent_status = "Completed" + address_doc.save() + try: + new_sales_order = make_sales_order(doc.name) + new_sales_order.custom_requires_half_payment = doc.requires_half_payment + new_sales_order.insert() + print("DEBUG: Sales Order created successfully:", new_sales_order.name) + except Exception as e: + print("ERROR creating Sales Order from Estimate:", str(e)) + frappe.log_error(f"Error creating Sales Order from Estimate {doc.name}: {str(e)}", "Estimate on_submit Error") diff --git a/custom_ui/events/onsite_meeting.py b/custom_ui/events/onsite_meeting.py index eb4d03b..c05ebab 100644 --- a/custom_ui/events/onsite_meeting.py +++ b/custom_ui/events/onsite_meeting.py @@ -14,4 +14,8 @@ def after_save(doc, method): print("DEBUG: Meeting marked as Completed, updating Address status") address_doc = frappe.get_doc("Address", doc.address) address_doc.custom_onsite_meeting_scheduled = "Completed" - address_doc.save() \ No newline at end of file + address_doc.save() + return + if doc.status != "Scheduled" and doc.start_time and doc.end_time: + doc.status = "Scheduled" + doc.save() \ No newline at end of file diff --git a/custom_ui/events/sales_order.py b/custom_ui/events/sales_order.py new file mode 100644 index 0000000..4bb71ff --- /dev/null +++ b/custom_ui/events/sales_order.py @@ -0,0 +1,6 @@ +import frappe + +def after_insert(doc, method): + print(doc.as_dict()) + # Create Invoice and Project from Sales Order + \ No newline at end of file diff --git a/custom_ui/hooks.py b/custom_ui/hooks.py index 470be07..07112af 100644 --- a/custom_ui/hooks.py +++ b/custom_ui/hooks.py @@ -161,14 +161,18 @@ add_to_apps_screen = [ doc_events = { "On-Site Meeting": { "after_insert": "custom_ui.events.onsite_meeting.after_insert", - "after_save": "custom_ui.events.onsite_meeting.after_save" + "on_update": "custom_ui.events.onsite_meeting.after_save" }, "Address": { "after_insert": "custom_ui.events.address.after_insert" }, "Quotation": { "after_insert": "custom_ui.events.estimate.after_insert", - "after_save": "custom_ui.events.estimate.after_save" + "on_update": "custom_ui.events.estimate.after_save", + "after_submit": "custom_ui.events.estimate.after_submit" + }, + "Sales Order": { + "after_insert": "custom_ui.events.sales_order.after_insert" } } diff --git a/custom_ui/install.py b/custom_ui/install.py index 32d7f8f..4608a1e 100644 --- a/custom_ui/install.py +++ b/custom_ui/install.py @@ -178,6 +178,15 @@ def add_custom_fields(): default=0, insert_after="custom_installation_address" ) + ], + "Sales Order": [ + dict( + fieldname="requires_half_payment", + label="Requires Half Payment", + fieldtype="Check", + default=0, + insert_after="custom_installation_address" + ) ] } @@ -239,29 +248,55 @@ def update_address_fields(): print(f"\nšŸ“ Updating fields for {total_addresses} addresses...") - # Verify custom fields exist by checking the meta - address_meta = frappe.get_meta("Address") - required_fields = ['full_address', 'custom_onsite_meeting_scheduled', - 'custom_estimate_sent_status', 'custom_job_status', - 'custom_payment_received_status'] - + # 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"] + ], + "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"] + ] + } + missing_fields = [] - for field in required_fields: - if not address_meta.has_field(field): - missing_fields.append(field) - + 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(f"\nāŒ Missing custom fields: {', '.join(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...") # Field update counters field_counters = { 'full_address': 0, - 'latitude': 0, - 'longitude': 0, 'custom_onsite_meeting_scheduled': 0, 'custom_estimate_sent_status': 0, 'custom_job_status': 0, @@ -270,6 +305,9 @@ def update_address_fields(): 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): # Calculate progress progress_percentage = int((index / total_addresses) * 100) @@ -277,13 +315,25 @@ def update_address_fields(): filled_length = int(bar_length * index // total_addresses) bar = 'ā–ˆ' * filled_length + 'ā–‘' * (bar_length - filled_length) - # Print progress bar with field update count - print(f"\ršŸ“Š Progress: [{bar}] {progress_percentage:3d}% ({index}/{total_addresses}) | Fields Updated: {total_field_updates} - Processing: {name[:25]}...", end='', flush=True) - + # Print a three-line, refreshing progress block to avoid terminal wrap + 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]}..." + + if index == 1: + # Save cursor position at the start of the progress block + print("\033[s", end='') + else: + # Restore to the saved cursor position to rewrite the three-line block + print("\033[u", end='') + + print(f"\r\033[K{progress_line}") + print(f"\r\033[K{counters_line}") + print(f"\r\033[K{detail_line}", end='' if index != total_addresses else '\n', flush=True) + should_update = False address = frappe.get_doc("Address", name) current_address_updates = 0 - current_address_updates = 0 # Use getattr with default values instead of direct attribute access if not getattr(address, 'full_address', None): @@ -310,14 +360,15 @@ def update_address_fields(): job_status = "Not Started" payment_received = "Not Started" - onsite_meetings = frappe.get_all("On-Site Meeting", fields=["docstatus"],filters={"address": address.address_title}) + onsite_meetings = frappe.get_all("On-Site Meeting", fields=[onsite_status_field], filters={"address": address.address_title}) if onsite_meetings and onsite_meetings[0]: - onsite_meeting = "Completed" if onsite_meetings[0]["docstatus"] == 1 else "In Progress" + 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"], filters={"custom_installation_address": address.address_title}) - if estimates and estimates[0] and estimates[0]["custom_sent"] == 1 and estimates[0]["docstatus"] == 1: + 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 estimates[0]["docstatus"] != 1: + 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.address_title, "project_template": "SNW Install"}) @@ -367,8 +418,6 @@ def update_address_fields(): print(f" • Total field updates: {total_field_updates:,}") print(f"\nšŸ“ Field-specific updates:") print(f" • Full Address: {field_counters['full_address']:,}") - print(f" • Latitude: {field_counters['latitude']:,}") - print(f" • Longitude: {field_counters['longitude']:,}") 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']:,}") diff --git a/custom_ui/templates/estimates/accepted.html b/custom_ui/templates/estimates/accepted.html new file mode 100644 index 0000000..f67e684 --- /dev/null +++ b/custom_ui/templates/estimates/accepted.html @@ -0,0 +1,86 @@ + + + + + + Quotation Accepted + + + + +
+
āœ“
+

Thank You, {{ doc.party_name or doc.customer }}!

+

You accepted the Quote! You will receive a payment link shortly.

+
+ + \ No newline at end of file diff --git a/custom_ui/templates/estimates/error.html b/custom_ui/templates/estimates/error.html new file mode 100644 index 0000000..c264dc9 --- /dev/null +++ b/custom_ui/templates/estimates/error.html @@ -0,0 +1,74 @@ + + + + + + Error + + + + +
+
āš ļø
+

Oops! Something went wrong.

+

We're sorry, but an error occurred. Please try again later or contact support if the problem persists.

+
+ + \ No newline at end of file diff --git a/custom_ui/templates/estimates/rejected.html b/custom_ui/templates/estimates/rejected.html new file mode 100644 index 0000000..335953c --- /dev/null +++ b/custom_ui/templates/estimates/rejected.html @@ -0,0 +1,87 @@ + + + + + + Quotation Rejected + + + + +
+
šŸ“ž
+

We're Sorry, {{ doc.party_name or doc.customer }}

+

We understand that our quote didn't meet your needs this time. We'd still love to discuss how we can help with your project!

+

Please don't hesitate to reach out:

+
+

Phone: [Your Company Phone Number]

+

Email: [Your Company Email]

+

Website: [Your Company Website]

+
+
+ + \ No newline at end of file diff --git a/custom_ui/templates/estimates/request-call.html b/custom_ui/templates/estimates/request-call.html new file mode 100644 index 0000000..bf945e2 --- /dev/null +++ b/custom_ui/templates/estimates/request-call.html @@ -0,0 +1,86 @@ + + + + + + Call Requested + + + + +
+
šŸ“ž
+

Thank You, {{ doc.party_name or doc.customer }}!

+

Thank you for your response! Someone from our team will reach out to you soon to discuss your project.

+
+ + \ No newline at end of file diff --git a/frontend/src/components/common/DataTable.vue b/frontend/src/components/common/DataTable.vue index ff8340b..f23b44a 100644 --- a/frontend/src/components/common/DataTable.vue +++ b/frontend/src/components/common/DataTable.vue @@ -216,8 +216,8 @@ [5, 10, 20, 50], + }, filters: { type: Object, default: () => ({ @@ -424,6 +437,10 @@ const props = defineProps({ type: String, default: "pi pi-spinner pi-spin", }, + scrollHeight: { + type: String, + default: "70vh", + }, // Auto-connect to global loading store useGlobalLoading: { type: Boolean, @@ -479,15 +496,18 @@ const loading = computed(() => { // Get current rows per page from pagination store or default const currentRows = computed(() => { + if (!props.paginator) { + return props.data?.length || 0; + } if (props.lazy) { return paginationStore.getTablePagination(props.tableName).rows; } - return 10; // Default for non-lazy tables + return props.rows || 10; // Default for non-lazy tables }); // Get current first index for pagination synchronization const currentFirst = computed(() => { - if (props.lazy) { + if (props.lazy && props.paginator) { return paginationStore.getTablePagination(props.tableName).first; } return currentPageState.value.first; @@ -497,7 +517,7 @@ const currentFirst = computed(() => { onMounted(() => { filtersStore.initializeTableFilters(props.tableName, props.columns); filtersStore.initializeTableSorting(props.tableName); - if (props.lazy) { + if (props.lazy && props.paginator) { paginationStore.initializeTablePagination(props.tableName, { rows: 10, totalRecords: props.totalRecords, @@ -532,7 +552,7 @@ const filterRef = computed({ watch( () => props.totalRecords, (newTotal) => { - if (props.lazy && newTotal !== undefined) { + if (props.lazy && props.paginator && newTotal !== undefined) { // Force reactivity update for page controls selectedPageJump.value = ""; } @@ -603,6 +623,9 @@ const hasFilterChanges = computed(() => { }); const totalPages = computed(() => { + if (!props.paginator) { + return 1; + } if (props.lazy) { return paginationStore.getTotalPages(props.tableName); } diff --git a/frontend/src/components/pages/Estimate.vue b/frontend/src/components/pages/Estimate.vue index 1a014a7..ffe4d24 100644 --- a/frontend/src/components/pages/Estimate.vue +++ b/frontend/src/components/pages/Estimate.vue @@ -1,6 +1,9 @@