diff --git a/custom_ui/api/db/bid_meetings.py b/custom_ui/api/db/bid_meetings.py index 1e9f5fc..bc73060 100644 --- a/custom_ui/api/db/bid_meetings.py +++ b/custom_ui/api/db/bid_meetings.py @@ -1,7 +1,7 @@ import frappe import json from custom_ui.db_utils import build_error_response, build_success_response, process_filters, process_sorting -from custom_ui.services import DbService, ClientService, AddressService +from custom_ui.services import DbService, ClientService, AddressService, ContactService @frappe.whitelist() def get_week_bid_meetings(week_start, week_end): @@ -17,8 +17,10 @@ def get_week_bid_meetings(week_start, week_end): order_by="start_time asc" ) for meeting in meetings: - address_doc = frappe.get_doc("Address", meeting["address"]) + address_doc = AddressService.get_or_throw(meeting["address"]) meeting["address"] = address_doc.as_dict() + contact_doc = ContactService.get_or_throw(meeting["contact"]) if meeting.get("contact") else None + meeting["contact"] = contact_doc.as_dict() if contact_doc else None return build_success_response(meetings) except Exception as e: frappe.log_error(message=str(e), title="Get Week On-Site Meetings Failed") @@ -60,6 +62,13 @@ def get_unscheduled_bid_meetings(): filters={"status": "Unscheduled"}, order_by="creation desc" ) + for meeting in meetings: + address_doc = AddressService.get_or_throw(meeting["address"]) + meeting["address"] = address_doc.as_dict() + # client_doc = ClientService.get_client_doctype(meeting["party_name"]) + # meeting["client"] = client_doc.as_dict() if client_doc else None + contact_doc = ContactService.get_or_throw(meeting["contact"]) if meeting.get("contact") else None + meeting["contact"] = contact_doc.as_dict() if contact_doc else None return build_success_response(meetings) except Exception as e: frappe.log_error(message=str(e), title="Get Unscheduled On-Site Meetings Failed") @@ -75,8 +84,11 @@ def get_bid_meeting(name): # Get the full address data if meeting_dict.get("address"): - address_doc = frappe.get_doc("Address", meeting_dict["address"]) + address_doc = AddressService.get_or_throw(meeting_dict["address"]) meeting_dict["address"] = address_doc.as_dict() + if meeting_dict.get("contact"): + contact_doc = ContactService.get_or_throw(meeting_dict["contact"]) + meeting_dict["contact"] = contact_doc.as_dict() return build_success_response(meeting_dict) except frappe.DoesNotExistError: @@ -127,37 +139,30 @@ def create_bid_meeting(data): @frappe.whitelist() def update_bid_meeting(name, data): """Update an existing On-Site Meeting.""" - defualts = { - "address": None, - "start_time": None, - "end_time": None, - "notes": None, - "assigned_employee": None, - "completed_by": None, - "contact": None, - "status": None - } try: if isinstance(data, str): data = json.loads(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) + + # Only update fields that are explicitly provided in the data 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") - elif key in ["assigned_employee", "completed_by"]: - value = frappe.db.get_value("Employee", {"employee_name": value}, "name") + print(f"DEBUG: Updating field '{key}' to value '{value}'") + if key == "address" and value is not None: + # Convert full address to address name + value = frappe.db.get_value("Address", {"full_address": value}, "name") meeting.set(key, value) + elif key in ["assigned_employee", "completed_by"] and value is not None: + # Convert employee name to employee ID + value = frappe.db.get_value("Employee", {"employee_name": value}, "name") + meeting.set(key, value) + else: + # For all other fields, set the value as-is (including None to clear fields) + meeting.set(key, value) + print(f"DEBUG: Field '{key}' updated to '{meeting.get(key)}'") 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/api/db/clients.py b/custom_ui/api/db/clients.py index abcd6e4..39d5add 100644 --- a/custom_ui/api/db/clients.py +++ b/custom_ui/api/db/clients.py @@ -205,19 +205,20 @@ def get_clients_table_data(filters={}, sortings=[], page=1, page_size=10): addresses = [frappe.get_doc("Address", addr["name"]).as_dict() for addr in address_names] tableRows = [] for address in addresses: - is_lead = False + is_lead = address.customer_type == "Lead" + print("##########IS LEAD:", is_lead) 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", None) - if not customer_links: - customer_links = [link for link in links if link.link_doctype == "Lead"] if links else None - is_lead = True if customer_links else False - if not customer_name and not customer_links: + # customer_links = [link for link in links if link.link_doctype == "Customer"] if links else None + customer_name = address.get("customer_name") + # if not customer_links: + # customer_links = [link for link in links if link.link_doctype == "Lead"] if links else None + # is_lead = True if customer_links else False + # if not customer_name and not customer_links: + # customer_name = frappe.get_value("Lead", address.get("customer_name"), "custom_customer_name") + if is_lead: + # print("DEBUG: No customer to bill. Customer links found:", customer_links) customer_name = frappe.get_value("Lead", address.get("customer_name"), "custom_customer_name") - elif not customer_name and customer_links: - print("DEBUG: No customer to bill. Customer links found:", customer_links) - customer_name = frappe.get_value("Lead", customer_links[0].link_name, "custom_customer_name") if is_lead else customer_links[0].link_name tableRow["id"] = address["name"] tableRow["customer_name"] = customer_name tableRow["address"] = ( @@ -225,6 +226,7 @@ def get_clients_table_data(filters={}, sortings=[], page=1, page_size=10): f"{' ' + address['address_line2'] if address['address_line2'] else ''} " f"{address['city']}, {address['state']} {address['pincode']}" ) + print("########IS LEAD @TABLE ROW:", is_lead) tableRow["client_type"] = "Lead" if is_lead else "Customer" # tableRow["appointment_scheduled_status"] = address.custom_onsite_meeting_scheduled # tableRow["estimate_sent_status"] = address.custom_estimate_sent_status @@ -308,6 +310,7 @@ def upsert_client(data): "phone": primary_contact.get("phone_number"), "custom_customer_name": customer_name, "customer_type": customer_type, + "address_type": "Billing", "companies": [{ "company": data.get("company_name") }] } diff --git a/custom_ui/api/db/estimates.py b/custom_ui/api/db/estimates.py index 261521f..82d2f36 100644 --- a/custom_ui/api/db/estimates.py +++ b/custom_ui/api/db/estimates.py @@ -4,7 +4,7 @@ from custom_ui.api.db.general import get_doc_history 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 from custom_ui.api.db.clients import check_if_customer, convert_lead_to_customer -from custom_ui.services import DbService, ClientService, AddressService +from custom_ui.services import DbService, ClientService, AddressService, ContactService # =============================================================================== # ESTIMATES & INVOICES API METHODS @@ -145,17 +145,18 @@ def send_estimate_email(estimate_name): print("DEBUG: Sending estimate email for:", estimate_name) quotation = frappe.get_doc("Quotation", estimate_name) - party_exists = frappe.db.exists(quotation.quotation_to, quotation.party_name) - if not party_exists: + + if not DbService.exists("Contact", quotation.contact_person): return build_error_response("No email found for the customer.", 400) - party = frappe.get_doc(quotation.quotation_to, quotation.party_name) + party = ContactService.get_or_throw(quotation.contact_person) - email = None - if (getattr(party, 'email_id', None)): - email = party.email_id - elif (getattr(party, 'contact_ids', None) and len(party.email_ids) > 0): - primary = next((e for e in party.email_ids if e.is_primary), None) - email = primary.email_id if primary else party.email_ids[0].email_id + email = quotation.contact_email or None + if not email: + if (getattr(party, 'email_id', None)): + email = party.email_id + elif (getattr(party, 'email_ids', None) and len(party.email_ids) > 0): + primary = next((e for e in party.email_ids if e.is_primary), None) + email = primary.email_id if primary else party.email_ids[0].email_id if not email and quotation.custom_job_address: address = frappe.get_doc("Address", quotation.custom_job_address) @@ -383,9 +384,10 @@ def upsert_estimate(data): try: data = json.loads(data) if isinstance(data, str) else data print("DEBUG: Upsert estimate data:", data) - + address_doc = AddressService.get_or_throw(data.get("address_name")) estimate_name = data.get("estimate_name") - client_doctype = ClientService.get_client_doctype(data.get("customer")) + client_doctype = ClientService.get_client_doctype(address_doc.customer_name) + print("DEBUG: Retrieved client doctype:", client_doctype) project_template = data.get("project_template", None) # If estimate_name exists, update existing estimate @@ -430,17 +432,16 @@ def upsert_estimate(data): else: print("DEBUG: Creating new estimate") print("DEBUG: Retrieved address name:", data.get("address_name")) - client_doctype = ClientService.get_client_doctype(data.get("customer")) new_estimate = frappe.get_doc({ "doctype": "Quotation", "custom_requires_half_payment": data.get("requires_half_payment", 0), "custom_job_address": data.get("address_name"), "custom_current_status": "Draft", "contact_email": data.get("contact_email"), - "party_name": data.get("customer"), - "quotation_to": client_doctype, + "party_name": data.get("contact_name"), + "quotation_to": "Contact", "company": data.get("company"), - "customer": data.get("customer"), + "actual_customer_name": address_doc.customer_name, "customer_type": client_doctype, "customer_address": data.get("address_name"), "contact_person": data.get("contact_name"), @@ -457,9 +458,12 @@ def upsert_estimate(data): "discount_amount": item.get("discount_amount") or item.get("discountAmount", 0), "discount_percentage": item.get("discount_percentage") or item.get("discountPercentage", 0) }) + # Iterate through every field and print it out, I need to see if there is any field that is a Dynamic link saying Customer + for fieldname, value in new_estimate.as_dict().items(): + print(f"DEBUG: Field '{fieldname}': {value}") new_estimate.insert() - AddressService.append_link(data.get("address_name"), "quotations", "quotation", new_estimate.name) - ClientService.append_link(data.get("customer"), "quotations", "quotation", new_estimate.name) + # AddressService.append_link(data.get("address_name"), "quotations", "quotation", new_estimate.name) + # ClientService.append_link(data.get("customer"), "quotations", "quotation", new_estimate.name) print("DEBUG: New estimate created with name:", new_estimate.name) return build_success_response(new_estimate.as_dict()) except Exception as e: diff --git a/custom_ui/events/client.py b/custom_ui/events/client.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_ui/events/estimate.py b/custom_ui/events/estimate.py index 9327607..40df9ae 100644 --- a/custom_ui/events/estimate.py +++ b/custom_ui/events/estimate.py @@ -5,8 +5,10 @@ from custom_ui.services import DbService, AddressService, ClientService def after_insert(doc, method): print("DEBUG: After insert hook triggered for Quotation:", doc.name) AddressService.append_link_v2( - doc.custom_job_address, - {"quotations": {"quotation": doc.name, "project_template": doc.custom_project_template}} + doc.custom_job_address, "quotations", {"quotation": doc.name, "project_template": doc.custom_project_template} + ) + ClientService.append_link_v2( + doc.actual_customer_name, "quotations", {"quotation": doc.name, "project_template": doc.custom_project_template} ) template = doc.custom_project_template or "Other" if template == "Other": @@ -20,18 +22,24 @@ def after_insert(doc, method): ) def before_insert(doc, method): - print("DEBUG: Before insert hook triggered for Quotation:", doc.name) - # if doc.custom_project_template == "SNW Install": - # print("DEBUG: Quotation uses SNW Install template, setting initial Address status to 'In Progress'.") - # address_doc = AddressService.get_or_throw(doc.custom_job_address) - # if "SNW Install" in [link.project_template for link in address_doc.quotations]: - # raise frappe.ValidationError("An Estimate with project template 'SNW Install' is already linked to this address.") + print("DEBUG: Before insert hook triggered for Quotation:", doc) + print("DEBUG: CHECKING CUSTOMER TYPE") + print(doc.customer_type) + print("DEBUG: CHECKING CUSTOMER NAME") + print(doc.actual_customer_name) + print("Quotation_to:", doc.quotation_to) + # print("Party_type:", doc.party_type) + if doc.custom_project_template == "SNW Install": + print("DEBUG: Quotation uses SNW Install template, making sure no duplicate linked estimates.") + address_doc = AddressService.get_or_throw(doc.custom_job_address) + if "SNW Install" in [link.project_template for link in address_doc.quotations]: + raise frappe.ValidationError("An Estimate with project template 'SNW Install' is already linked to this address.") def before_submit(doc, method): print("DEBUG: Before submit hook triggered for Quotation:", doc.name) if doc.custom_project_template == "SNW Install": print("DEBUG: Quotation uses SNW Install template.") - if doc.custom_current_status == "Estimate Sent": + if doc.custom_sent == 1: print("DEBUG: Current status is 'Estimate Sent', updating Address status to 'Sent'.") AddressService.update_value( doc.custom_job_address, @@ -47,14 +55,18 @@ def on_update_after_submit(doc, method): print("DEBUG: Quotation marked as Won, updating current status.") if doc.customer_type == "Lead": print("DEBUG: Customer is a Lead, converting to Customer and updating Quotation.") - new_customer = ClientService.convert_lead_to_customer(doc.customer, update_quotations=False) - doc.customer = new_customer.name + new_customer = ClientService.convert_lead_to_customer(doc.actual_customer_name, update_quotations=False) + doc.actual_customer_name = new_customer.name doc.customer_type = "Customer" + new_customer.reload() + ClientService.append_link_v2( + new_customer.name, "quotations", {"quotation": doc.name} + ) doc.save() 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.customer = doc.customer + new_sales_order.customer = doc.actual_customer_name # new_sales_order.custom_installation_address = doc.custom_installation_address # new_sales_order.custom_job_address = doc.custom_job_address new_sales_order.payment_schedule = [] @@ -62,8 +74,11 @@ def on_update_after_submit(doc, method): new_sales_order.set_payment_schedule() print("DEBUG: Inserting Sales Order:", new_sales_order.as_dict()) new_sales_order.delivery_date = new_sales_order.transaction_date + backup = new_sales_order.customer_address + new_sales_order.customer_address = None new_sales_order.insert() print("DEBUG: Submitting Sales Order") + new_sales_order.customer_address = backup new_sales_order.submit() frappe.db.commit() print("DEBUG: Sales Order created successfully:", new_sales_order.name) diff --git a/custom_ui/events/onsite_meeting.py b/custom_ui/events/onsite_meeting.py index 15ae2bd..334a514 100644 --- a/custom_ui/events/onsite_meeting.py +++ b/custom_ui/events/onsite_meeting.py @@ -22,7 +22,7 @@ def after_insert(doc, method): def before_save(doc, method): print("DEBUG: Before Save Triggered for On-Site Meeting") - if doc.status != "Scheduled" and doc.start_time and doc.end_time: + if doc.status != "Scheduled" and doc.start_time and doc.end_time and doc.status != "Completed": print("DEBUG: Meeting has start and end time, setting status to Scheduled") doc.status = "Scheduled" if doc.project_template == "SNW Install": diff --git a/custom_ui/fixtures/doctype.json b/custom_ui/fixtures/doctype.json index a40c73b..5455fe4 100644 --- a/custom_ui/fixtures/doctype.json +++ b/custom_ui/fixtures/doctype.json @@ -114,8 +114,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "084d85c4c6bd8aa7a867582454f1fc78", - "modified": "2026-01-13 11:55:00.125952", + "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", + "modified": "2026-01-15 10:05:57.065211", "module": "Custom", "name": "Lead Companies Link", "naming_rule": "", @@ -332,8 +332,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5f481f64a0f53ad40b09d8b5694265c1", - "modified": "2026-01-15 03:13:47.584878", + "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", + "modified": "2026-01-15 10:05:57.117683", "module": "Custom", "name": "Address Project Link", "naming_rule": "", @@ -550,8 +550,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5f481f64a0f53ad40b09d8b5694265c1", - "modified": "2026-01-15 03:12:40.401170", + "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", + "modified": "2026-01-15 10:05:57.173958", "module": "Custom", "name": "Address Quotation Link", "naming_rule": "", @@ -768,8 +768,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5f481f64a0f53ad40b09d8b5694265c1", - "modified": "2026-01-15 03:08:11.881037", + "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", + "modified": "2026-01-15 10:05:57.229451", "module": "Custom", "name": "Address On-Site Meeting Link", "naming_rule": "", @@ -922,8 +922,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5f481f64a0f53ad40b09d8b5694265c1", - "modified": "2026-01-15 00:40:38.392595", + "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", + "modified": "2026-01-15 10:05:57.280251", "module": "Custom", "name": "Address Sales Order Link", "naming_rule": "", @@ -1076,8 +1076,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5f481f64a0f53ad40b09d8b5694265c1", - "modified": "2026-01-15 00:40:38.443423", + "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", + "modified": "2026-01-15 10:05:57.332720", "module": "Custom", "name": "Contact Address Link", "naming_rule": "", @@ -1230,8 +1230,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5f481f64a0f53ad40b09d8b5694265c1", - "modified": "2026-01-15 00:40:38.493149", + "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", + "modified": "2026-01-15 10:05:57.385343", "module": "Custom", "name": "Lead On-Site Meeting Link", "naming_rule": "", @@ -1832,8 +1832,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5f481f64a0f53ad40b09d8b5694265c1", - "modified": "2026-01-15 00:40:38.563912", + "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", + "modified": "2026-01-15 10:05:57.457103", "module": "Selling", "name": "Quotation Template", "naming_rule": "", @@ -2330,8 +2330,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5f481f64a0f53ad40b09d8b5694265c1", - "modified": "2026-01-15 00:40:38.635350", + "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", + "modified": "2026-01-15 10:05:57.529451", "module": "Selling", "name": "Quotation Template Item", "naming_rule": "", @@ -2484,8 +2484,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5f481f64a0f53ad40b09d8b5694265c1", - "modified": "2026-01-15 00:40:38.685490", + "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", + "modified": "2026-01-15 10:05:57.580626", "module": "Custom UI", "name": "Customer Company Link", "naming_rule": "", @@ -2638,8 +2638,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5f481f64a0f53ad40b09d8b5694265c1", - "modified": "2026-01-15 00:40:38.735935", + "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", + "modified": "2026-01-15 10:05:57.630918", "module": "Custom UI", "name": "Customer Address Link", "naming_rule": "", @@ -2792,8 +2792,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5f481f64a0f53ad40b09d8b5694265c1", - "modified": "2026-01-15 00:40:38.785648", + "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", + "modified": "2026-01-15 10:05:57.681698", "module": "Custom UI", "name": "Customer Contact Link", "naming_rule": "", @@ -2946,8 +2946,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5f481f64a0f53ad40b09d8b5694265c1", - "modified": "2026-01-15 00:40:38.837258", + "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", + "modified": "2026-01-15 10:05:57.731378", "module": "Custom", "name": "Address Contact Link", "naming_rule": "", @@ -3100,8 +3100,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5f481f64a0f53ad40b09d8b5694265c1", - "modified": "2026-01-15 00:40:38.887850", + "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", + "modified": "2026-01-15 10:05:57.783154", "module": "Custom", "name": "Customer On-Site Meeting Link", "naming_rule": "", @@ -3254,8 +3254,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5f481f64a0f53ad40b09d8b5694265c1", - "modified": "2026-01-15 00:40:38.938600", + "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", + "modified": "2026-01-15 10:05:57.832605", "module": "Custom", "name": "Customer Project Link", "naming_rule": "", @@ -3408,8 +3408,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5f481f64a0f53ad40b09d8b5694265c1", - "modified": "2026-01-15 00:40:38.989639", + "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", + "modified": "2026-01-15 10:05:57.882511", "module": "Custom", "name": "Customer Quotation Link", "naming_rule": "", @@ -3562,8 +3562,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5f481f64a0f53ad40b09d8b5694265c1", - "modified": "2026-01-15 00:40:39.042414", + "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", + "modified": "2026-01-15 10:05:57.934072", "module": "Custom", "name": "Customer Sales Order Link", "naming_rule": "", @@ -3716,8 +3716,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5f481f64a0f53ad40b09d8b5694265c1", - "modified": "2026-01-15 00:40:39.092979", + "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", + "modified": "2026-01-15 10:05:57.984126", "module": "Custom", "name": "Lead Address Link", "naming_rule": "", @@ -3870,8 +3870,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5f481f64a0f53ad40b09d8b5694265c1", - "modified": "2026-01-15 00:40:39.144013", + "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", + "modified": "2026-01-15 10:05:58.034758", "module": "Custom", "name": "Lead Contact Link", "naming_rule": "", @@ -4024,8 +4024,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5f481f64a0f53ad40b09d8b5694265c1", - "modified": "2026-01-15 00:40:39.251239", + "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", + "modified": "2026-01-15 10:05:58.086245", "module": "Custom", "name": "Lead Quotation Link", "naming_rule": "", @@ -4178,8 +4178,8 @@ "make_attachments_public": 0, "max_attachments": 0, "menu_index": null, - "migration_hash": "5f481f64a0f53ad40b09d8b5694265c1", - "modified": "2026-01-15 00:40:39.304711", + "migration_hash": "717d2b4e3d605ae371d8c05bb650b364", + "modified": "2026-01-15 10:05:58.139052", "module": "Custom", "name": "Address Company Link", "naming_rule": "", diff --git a/custom_ui/install.py b/custom_ui/install.py index d90935b..10f9530 100644 --- a/custom_ui/install.py +++ b/custom_ui/install.py @@ -462,18 +462,20 @@ def add_custom_fields(): insert_after="custom_job_address" ), dict( - fieldname="customer", + fieldname="actual_customer_name", label="Customer", fieldtype="Dynamic Link", options="customer_type", - insert_after="from_onsite_meeting" + insert_after="from_onsite_meeting", + allow_on_submit=1 ), dict( fieldname="customer_type", label="Customer Type", fieldtype="Select", options="Customer\nLead", - insert_after="customer_name" + insert_after="customer_name", + allow_on_submit=1 ) ], "Sales Order": [ diff --git a/custom_ui/services/address_service.py b/custom_ui/services/address_service.py index 67de456..14d81d0 100644 --- a/custom_ui/services/address_service.py +++ b/custom_ui/services/address_service.py @@ -1,4 +1,5 @@ import frappe +import requests from .contact_service import ContactService, DbService class AddressService: @@ -164,6 +165,38 @@ class AddressService: """Set a link field for an address using a link dictionary.""" print(f"DEBUG: Setting link field {field} for Address {address_name} with link data {link}") address_doc = AddressService.get_or_throw(address_name) + print("DEBUG: Appending link:", link) address_doc.append(field, link) + print("DEBUG: Saving address document after appending link.") address_doc.save(ignore_permissions=True) - print(f"DEBUG: Set link field {field} for Address {address_name} with link data {link}") \ No newline at end of file + print(f"DEBUG: Set link field {field} for Address {address_name} with link data {link}") + + @staticmethod + def get_county_and_set(address_doc, save: bool = False): + """Get the county from the address document and set it if not already set.""" + if not address_doc.county: + print(f"DEBUG: Getting county for Address {address_doc.name}") + # Example logic to determine county from address fields + # This is a placeholder; actual implementation may vary + url = "https://geocoding.geo.cencus.gov/geocoder/geographies/coordinates" + params = { + "x": address_doc.longitude, + "y": address_doc.latitude, + "benchmark": "Public_AR_Current", + "vintage": "Current_Current", + "format": "json" + } + + r = requests.get(url, params=params, timeout=10) + data = r.json() + + try: + county = data['result']['geographies']['Counties'][0]['NAME'] + county_fips = data['result']['geographies']['Counties'][0]['GEOID'] + except (KeyError, IndexError): + return None + + return { + "county": county, + "county_fips": county_fips + } \ No newline at end of file diff --git a/custom_ui/services/client_service.py b/custom_ui/services/client_service.py index d972983..165b043 100644 --- a/custom_ui/services/client_service.py +++ b/custom_ui/services/client_service.py @@ -37,7 +37,19 @@ class ClientService: }) client_doc.save(ignore_permissions=True) print(f"DEBUG: Set link field {field} for client {client_doc.get('name')} to {link_doctype} {link_name}") - + + @staticmethod + def append_link_v2(client_name: str, field: str, link: dict): + """Set a link field for a client (Customer or Lead) using a link dictionary.""" + print(f"DEBUG: Setting link field {field} for client {client_name} with link data {link}") + client_doctype = ClientService.get_client_doctype(client_name) + client_doc = DbService.get_or_throw(client_doctype, client_name) + print("DEBUG: Appending link:", link) + client_doc.append(field, link) + print("DEBUG: Saving client document after appending link.") + client_doc.save(ignore_permissions=True) + print(f"DEBUG: Set link field {field} for client {client_doc.get('name')} with link data {link}") + @staticmethod def convert_lead_to_customer( lead_name: str, @@ -48,24 +60,78 @@ class ClientService: ): """Convert a Lead to a Customer.""" print(f"DEBUG: Converting Lead {lead_name} to Customer") - lead_doc = DbService.get_or_throw("Lead", lead_name) - customer_doc = make_customer(lead_doc.name) - customer_doc.insert(ignore_permissions=True) - if update_addresses: - for address in lead_doc.get("addresses", []): - address_doc = AddressService.get_or_throw(address.get("address")) - AddressService.link_address_to_customer(address_doc, "Customer", customer_doc.name) - if update_contacts: - for contact in lead_doc.get("contacts", []): - contact_doc = ContactService.get_or_throw(contact.get("contact")) - ContactService.link_contact_to_customer(contact_doc, "Customer", customer_doc.name) - if update_quotations: - for quotation in lead_doc.get("quotations", []): - quotation_doc = EstimateService.get_or_throw(quotation.get("quotation")) - EstimateService.link_estimate_to_customer(quotation_doc, "Customer", customer_doc.name) - if update_onsite_meetings: - for meeting in lead_doc.get("onsite_meetings", []): - meeting_doc = OnSiteMeetingService.get_or_throw(meeting.get("onsite_meeting")) - OnSiteMeetingService.link_onsite_meeting_to_customer(meeting_doc, "Customer", customer_doc.name) - print(f"DEBUG: Converted Lead {lead_name} to Customer {customer_doc.name}") - return customer_doc \ No newline at end of file + try: + lead_doc = DbService.get_or_throw("Lead", lead_name) + print(f"DEBUG: Retrieved Lead document: {lead_doc.name}") + + print("DEBUG: RUNNING make_customer()") + customer_doc = make_customer(lead_doc.name) + print(f"DEBUG: make_customer() returned document type: {type(customer_doc)}") + print(f"DEBUG: Customer doc name: {customer_doc.name if hasattr(customer_doc, 'name') else 'NO NAME'}") + + print("DEBUG: Calling customer_doc.insert()") + customer_doc.insert(ignore_permissions=True) + print(f"DEBUG: Customer inserted successfully: {customer_doc.name}") + + frappe.db.commit() + print("DEBUG: Database committed after customer insert") + print("DEBUG: CREATED CUSTOMER:", customer_doc.as_dict()) + if update_addresses: + print("DEBUG: Lead_doc addresses:", lead_doc.get("addresses", [])) + print(f"DEBUG: Updating addresses. Count: {len(lead_doc.get('properties', []))}") + for address in lead_doc.get("properties", []): + try: + print(f"DEBUG: Processing address: {address.get('address')}") + ClientService.append_link_v2(customer_doc.name, "properties", {"address": address.get("address")}) + address_doc = AddressService.get_or_throw(address.get("address")) + AddressService.link_address_to_customer(address_doc, "Customer", customer_doc.name) + print(f"DEBUG: Linked address {address.get('address')} to customer") + except Exception as e: + print(f"ERROR: Failed to link address {address.get('address')}: {str(e)}") + frappe.log_error(f"Address linking error: {str(e)}", "convert_lead_to_customer") + + if update_contacts: + print(f"DEBUG: Updating contacts. Count: {len(lead_doc.get('contacts', []))}") + for contact in lead_doc.get("contacts", []): + try: + print(f"DEBUG: Processing contact: {contact.get('contact')}") + ClientService.append_link_v2(customer_doc.name, "contacts", {"contact": contact.get("contact")}) + contact_doc = ContactService.get_or_throw(contact.get("contact")) + ContactService.link_contact_to_customer(contact_doc, "Customer", customer_doc.name) + print(f"DEBUG: Linked contact {contact.get('contact')} to customer") + except Exception as e: + print(f"ERROR: Failed to link contact {contact.get('contact')}: {str(e)}") + frappe.log_error(f"Contact linking error: {str(e)}", "convert_lead_to_customer") + + if update_quotations: + print(f"DEBUG: Updating quotations. Count: {len(lead_doc.get('quotations', []))}") + for quotation in lead_doc.get("quotations", []): + try: + print(f"DEBUG: Processing quotation: {quotation.get('quotation')}") + ClientService.append_link_v2(customer_doc.name, "quotations", {"quotation": quotation.get("quotation")}) + quotation_doc = EstimateService.get_or_throw(quotation.get("quotation")) + EstimateService.link_estimate_to_customer(quotation_doc, "Customer", customer_doc.name) + print(f"DEBUG: Linked quotation {quotation.get('quotation')} to customer") + except Exception as e: + print(f"ERROR: Failed to link quotation {quotation.get('quotation')}: {str(e)}") + frappe.log_error(f"Quotation linking error: {str(e)}", "convert_lead_to_customer") + + if update_onsite_meetings: + print(f"DEBUG: Updating onsite meetings. Count: {len(lead_doc.get('onsite_meetings', []))}") + for meeting in lead_doc.get("onsite_meetings", []): + try: + print(f"DEBUG: Processing onsite meeting: {meeting.get('onsite_meeting')}") + meeting_doc = DbService.get_or_throw("On-Site Meeting",meeting.get("onsite_meeting")) + ClientService.append_link_v2(customer_doc.name, "onsite_meetings", {"onsite_meeting": meeting.get("onsite_meeting")}) + OnSiteMeetingService.link_onsite_meeting_to_customer(meeting_doc, "Customer", customer_doc.name) + print(f"DEBUG: Linked onsite meeting {meeting.get('onsite_meeting')} to customer") + except Exception as e: + print(f"ERROR: Failed to link onsite meeting {meeting.get('onsite_meeting')}: {str(e)}") + frappe.log_error(f"Onsite meeting linking error: {str(e)}", "convert_lead_to_customer") + print(f"DEBUG: Converted Lead {lead_name} to Customer {customer_doc.name}") + return customer_doc + + except Exception as e: + print(f"ERROR: Exception in convert_lead_to_customer: {str(e)}") + frappe.log_error(f"convert_lead_to_customer failed: {str(e)}", "ClientService") + raise \ No newline at end of file diff --git a/frontend/src/components/calendar/bids/ScheduleBid.vue b/frontend/src/components/calendar/bids/ScheduleBid.vue index d81f0e0..ba63a30 100644 --- a/frontend/src/components/calendar/bids/ScheduleBid.vue +++ b/frontend/src/components/calendar/bids/ScheduleBid.vue @@ -173,21 +173,26 @@ draggable="true" @dragstart="handleDragStart($event, meeting)" @dragend="handleDragEnd($event)" + @click="showMeetingDetails(meeting)" > - -
{{ meeting.address }}
-
+ +
+ mdi-map-marker + {{ meeting.address?.fullAddress || meeting.address }} +
+
+ mdi-account + {{ meeting.contact.name }} +
+
+ mdi-file-document + {{ meeting.projectTemplate }} +
+
{{ meeting.status }}
-
- {{ meeting.notes }} -
-
- mdi-account - {{ meeting.assigned_employee }} -
@@ -876,17 +881,12 @@ const loadWeekMeetings = async () => { ? `${startDateTime.getHours().toString().padStart(2, "0")}:${startDateTime.getMinutes().toString().padStart(2, "0")}` : null; + // Return the full meeting object with calendar-specific fields added return { + ...meeting, // Keep all original fields id: meeting.name, - name: meeting.name, date: date, scheduledTime: time, - address: meeting.address, - notes: meeting.notes, - assigned_employee: meeting.assignedEmployee, - status: meeting.status, - startTime: meeting.startTime, - endTime: meeting.endTime, }; }) .filter((meeting) => meeting.date && meeting.scheduledTime); // Only include meetings with valid date/time @@ -1274,6 +1274,30 @@ watch( align-items: center; } +.unscheduled-address, +.unscheduled-contact, +.unscheduled-project { + font-size: 0.8em; + color: #666; + margin-bottom: 4px; + display: flex; + align-items: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.unscheduled-address { + font-weight: 600; + color: #1976d2; +} + +.unscheduled-status { + margin-top: 6px; + display: flex; + justify-content: flex-start; +} + .calendar-section { flex: 1; overflow: auto; diff --git a/frontend/src/components/clientSubPages/AddressInformationForm.vue b/frontend/src/components/clientSubPages/AddressInformationForm.vue index 5689aa6..4d0ee9a 100644 --- a/frontend/src/components/clientSubPages/AddressInformationForm.vue +++ b/frontend/src/components/clientSubPages/AddressInformationForm.vue @@ -1,6 +1,7 @@