diff --git a/custom_ui/api/db/addresses.py b/custom_ui/api/db/addresses.py index 51cbc5c..055ea3c 100644 --- a/custom_ui/api/db/addresses.py +++ b/custom_ui/api/db/addresses.py @@ -74,4 +74,52 @@ def get_addresses(fields=["*"], filters={}): return build_success_response(addresses) except Exception as e: frappe.log_error(message=str(e), title="Get Addresses Failed") - return build_error_response(str(e), 500) \ No newline at end of file + return build_error_response(str(e), 500) + + +def create_address(address_data): + """Create a new address.""" + address = frappe.get_doc({ + "doctype": "Address", + **address_data + }) + address.insert(ignore_permissions=True) + return address + +def address_exists(address_line1, address_line2, city, state, pincode): + """Check if an address with the given details already exists.""" + filters = { + "address_line1": address_line1, + "address_line2": address_line2, + "city": city, + "state": state, + "pincode": pincode + } + return frappe.db.exists("Address", filters) 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', '')}" + +def create_address_links(address_doc, client_doc, contact_docs): + print("#####DEBUG: Linking customer to address.") + print("#####DEBUG: Client Doc:", client_doc.as_dict(), "Address Doc:", address_doc.as_dict(), "Contact Docs:", [c.as_dict() for c in contact_docs]) + address_doc.append("links", { + "link_doctype": client_doc.doctype, + "link_name": client_doc.name + }) + setattr(address_doc, "custom_customer_to_bill" if client_doc.doctype == "Customer" else "lead_name", client_doc.name) + # Address -> Contact + print("#####DEBUG: Linking contacts to address.") + address_doc.custom_contact = next((c.name for c in contact_docs if c.is_primary_contact), contact_docs[0].name) + for contact_doc in contact_docs: + address_doc.append("custom_linked_contacts", { + "contact": contact_doc.name, + "email": contact_doc.email_id, + "phone": contact_doc.phone, + "role": contact_doc.role + }) + address_doc.append("links", { + "link_doctype": "Contact", + "link_name": contact_doc.name + }) + address_doc.save(ignore_permissions=True) \ No newline at end of file diff --git a/custom_ui/api/db/clients.py b/custom_ui/api/db/clients.py index 8ce616b..a3145fc 100644 --- a/custom_ui/api/db/clients.py +++ b/custom_ui/api/db/clients.py @@ -1,5 +1,8 @@ import frappe, json -from custom_ui.db_utils import build_error_response, process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, map_lead_client +from custom_ui.db_utils import build_error_response, process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, map_lead_client, build_address_title +from erpnext.crm.doctype.lead.lead import make_customer +from custom_ui.api.db.addresses import address_exists, create_address, create_address_links +from custom_ui.api.db.contacts import check_and_get_contact, create_contact, create_contact_links # =============================================================================== # CLIENT MANAGEMENT API METHODS @@ -94,15 +97,9 @@ def get_client(client_name): print("DEBUG: get_client called with client_name:", client_name) try: clientData = {"addresses": [], "contacts": [], "jobs": [], "sales_invoices": [], "payment_entries": [], "sales_orders": [], "tasks": []} - client_exists = frappe.db.exists("Customer", client_name) - if client_exists: - customer = frappe.get_doc("Customer", client_name) - else: - print("DEBUG: Client not found as Customer. Checking Lead.") - lead_name = frappe.db.get_all("Lead", pluck="name", filters={"lead_name": client_name})[0] - customer = frappe.get_doc("Lead", lead_name) - if not customer: - return build_error_response(f"Client '{client_name}' not found as Customer or Lead.", 404) + customer = check_and_get_client_doc(client_name) + if not customer: + return build_error_response(f"Client with name '{client_name}' does not exist.", 404) print("DEBUG: Retrieved customer/lead document:", customer.as_dict()) clientData = {**clientData, **customer.as_dict()} if customer.doctype == "Lead": @@ -117,7 +114,7 @@ def get_client(client_name): "Dynamic Link", filters={ "link_doctype": "Lead", - "link_name": lead_name, + "link_name": customer.name, "parenttype": ["in", ["Address", "Contact"]], }, fields=[ @@ -218,67 +215,55 @@ def upsert_client(data): """Create or update a client (customer and address).""" try: data = json.loads(data) + print("#####DEBUG: Upsert client data received:", data) + if address_exists( + data.get("address_line1"), + data.get("address_line2"), + data.get("city"), + data.get("state"), + data.get("pincode") + ): + return build_error_response("This address already exists. Please use a different address or search for the address to find the associated client.", 400) # Handle customer creation/update - print("#####DEBUG: Upsert client data received:", data) - print("#####DEBUG: Checking for existing customer with name:", data.get("customer_name")) customer = frappe.db.exists("Customer", {"customer_name": data.get("customer_name")}) - if not customer: print("#####DEBUG: No existing customer found. Checking for existing lead") customer = frappe.db.exists("Lead", {"lead_name": data.get("customer_name")}) + if not customer: + print("#####DEBUG: No existing lead found. Creating new lead.") + primary_contact = next((c for c in data.get("contacts", []) if c.get("is_primary")), None) + if not primary_contact: + return build_error_response("Primary contact information is required to create a new customer.", 400) + print("#####DEBUG: Primary contact found:", primary_contact) + client_doc = create_lead({ + "lead_name": data.get("customer_name"), + "first_name": primary_contact.get("first_name"), + "last_name": primary_contact.get("last_name"), + "email_id": primary_contact.get("email"), + "phone": primary_contact.get("phone_number"), + "customer_type": data.get("customer_type"), + "company": data.get("company") + }) + else: + print("#####DEBUG: Existing lead found:", customer) + client_doc = frappe.get_doc("Lead", customer) else: print("#####DEBUG: Existing customer found:", customer) - - if not customer: - print("#####DEBUG: No existing lead found. Creating new lead.") - is_individual = data.get("customer_type") == "Individual" - - primary_contact = next((c for c in data.get("contacts", []) if c.get("is_primary")), None) - if not primary_contact: - return build_error_response("Primary contact information is required to create a new customer.", 400) - print("#####DEBUG: Primary contact found:", primary_contact) - - new_lead_data = { - "doctype": "Lead", - "lead_name": data.get("customer_name"), - "first_name": primary_contact.get("first_name"), - "last_name": primary_contact.get("last_name"), - "email_id": primary_contact.get("email"), - "phone": primary_contact.get("phone_number"), - "customer_type": data.get("customer_type"), - "company": data.get("company") - } - print("#####DEBUG: New lead data prepared:", new_lead_data) - new_client_doc = frappe.get_doc(new_lead_data).insert(ignore_permissions=True) - else: - new_client_doc = frappe.get_doc("Customer", data.get("customer_name")) - print(f"#####DEBUG: {new_client_doc.doctype}:", new_client_doc.as_dict()) + client_doc = frappe.get_doc("Customer", customer) + print(f"#####DEBUG: {client_doc.doctype}:", client_doc.as_dict()) # Handle address creation - print("#####DEBUG: Checking for existing address for customer/lead:", data.get("customer_name")) - existing_address = frappe.db.exists( - "Address", - { - "address_line1": data.get("address_line1"), - "city": data.get("city"), - "state": data.get("state"), - }) - print("Existing address check:", existing_address) - if existing_address: - return build_error_response("Address already exists for this customer.", 400) - address_doc = frappe.get_doc({ - "doctype": "Address", - "address_title": data.get("address_title"), + address_doc = create_address({ + "address_title": build_address_title(data.get("customer_name"), data), "address_line1": data.get("address_line1"), "address_line2": data.get("address_line2"), "city": data.get("city"), "state": data.get("state"), "country": "United States", "pincode": data.get("pincode") - }).insert(ignore_permissions=True) - print("Address:", address_doc.as_dict()) + }) #Handle contact creation contact_docs = [] @@ -286,19 +271,22 @@ def upsert_client(data): if isinstance(contact_data, str): contact_data = json.loads(contact_data) print("#####DEBUG: Processing contact data:", contact_data) - contact_exists = frappe.db.exists("Contact", {"email_id": contact_data.get("email"), "phone": contact_data.get("phone_number")}) - print("Contact exists check:", contact_exists) - if not contact_exists: - contact_doc = frappe.get_doc({ - "doctype": "Contact", + contact_doc = check_and_get_contact( + contact_data.get("first_name"), + contact_data.get("last_name"), + contact_data.get("email"), + contact_data.get("phone_number") + ) + if not contact_doc: + print("#####DEBUG: No existing contact found. Creating new contact.") + contact_doc = create_contact({ "first_name": contact_data.get("first_name"), "last_name": contact_data.get("last_name"), # "email_id": contact_data.get("email"), # "phone": contact_data.get("phone_number"), - "custom_customer": customer_doc.name, "role": contact_data.get("contact_role", "Other"), "custom_email": contact_data.get("email"), - "is_primary_contact":1 if data.get("is_primary", False) else 0, + "is_primary_contact":1 if contact_data.get("is_primary", False) else 0, "email_ids": [{ "email_id": contact_data.get("email"), "is_primary": 1 @@ -308,85 +296,37 @@ def upsert_client(data): "is_primary_mobile_no": 1, "is_primary_phone": 1 }] - }).insert(ignore_permissions=True) - print("Created new contact:", contact_doc.as_dict()) - else: - contact_doc = frappe.get_doc("Contact", {"email_id": contact_data.get("email"), "phone": contact_data.get("phone_number")}) - print("Contact already exists:", contact_doc.as_dict()) + }) contact_docs.append(contact_doc) ##### Create links # Customer -> Address - if new_client_doc.doctype == "Customer": - print("#####DEBUG: Creating links between customer, address, and contacts.") - new_client_doc.append("custom_select_address", { + if client_doc.doctype == "Customer": + print("#####DEBUG: Linking address to customer.") + client_doc.append("custom_select_address", { "address_name": address_doc.name, - "address_line_1": address_doc.address_line1, - "city": address_doc.city, - "state": address_doc.state, - "pincode": address_doc.pincode }) # Customer -> Contact print("#####DEBUG: Linking contacts to customer.") for contact_doc in contact_docs: - print("Linking contact:", contact_doc.as_dict()) - print("with role:", contact_doc.role) - print("customer to append to:", new_client_doc.as_dict()) - new_client_doc.append("custom_add_contacts", { + client_doc.append("custom_add_contacts", { "contact": contact_doc.name, "email": contact_doc.custom_email, "phone": contact_doc.phone, "role": contact_doc.role }) - new_client_doc.append("links", { - "link_doctype": "Contact", - "link_name": contact_doc.name - } - ) - new_client_doc.save(ignore_permissions=True) + client_doc.save(ignore_permissions=True) # Address -> Customer/Lead - print("#####DEBUG: Linking address to customer.") - address_doc.append("links", { - "link_doctype": new_client_doc.doctype, - "link_name": new_client_doc.name - }) - if new_client_doc.doctype == "Lead": - address_doc.lead_name = new_client_doc.lead_name - - - # Address -> Contact - print("#####DEBUG: Linking address to contacts.") - address_doc.custom_contact = next((c.name for c in contact_docs if c.is_primary_contact), contact_docs[0].name) - for contact_doc in contact_docs: - address_doc.append("custom_linked_contacts", { - "contact": contact_doc.name, - "email": contact_doc.email_id, - "phone": contact_doc.phone, - "role": contact_doc.role - }) - - address_doc.save(ignore_permissions=True) + create_address_links(address_doc, client_doc, contact_docs) # Contact -> Customer/Lead & Address - print("#####DEBUG: Linking contacts to customer.") - for contact_doc in contact_docs: - contact_doc.address = address_doc.name - contact_doc.append("links", { - "link_doctype": new_client_doc.doctype, - "link_name": new_client_doc.name - }) - contact_doc.append("links", { - "link_doctype": "Address", - "link_name": address_doc.name - }) - contact_doc.custom_customer = new_client_doc.name - contact_doc.save(ignore_permissions=True) + create_contact_links(contact_docs, client_doc, address_doc) frappe.local.message_log = [] return build_success_response({ - "customer": new_client_doc.as_dict(), + "customer": client_doc.as_dict(), "address": address_doc.as_dict(), "contacts": [contact_doc.as_dict() for contact_doc in contact_docs] }) @@ -402,7 +342,48 @@ def get_client_names(search_term): search_pattern = f"%{search_term}%" client_names = frappe.db.get_all( "Customer", + filters={"customer_name": ["like", search_pattern]}, pluck="name") return build_success_response(client_names) except Exception as e: return build_error_response(str(e), 500) + +def check_if_customer(client_name): + """Check if the given client name corresponds to a Customer.""" + return frappe.db.exists("Customer", client_name) is not None + +def check_and_get_client_doc(client_name): + """Check if a client exists as Customer or Lead and return the document.""" + print("DEBUG: Checking for existing client with name:", client_name) + customer = None + if check_if_customer(client_name): + print("DEBUG: Client found as Customer.") + customer = frappe.get_doc("Customer", client_name) + else: + print("DEBUG: Client not found as Customer. Checking Lead.") + lead_name = frappe.db.get_all("Lead", pluck="name", filters={"lead_name": client_name}) + if lead_name: + print("DEBUG: Client found as Lead.") + customer = frappe.get_doc("Lead", lead_name[0]) + return customer + +def convert_lead_to_customer(lead_name): + lead = frappe.get_doc("Lead", lead_name) + customer = make_customer(lead) + customer.insert(ignore_permissions=True) + + +def create_lead(lead_data): + lead = frappe.get_doc({ + "doctype": "Lead", + **lead_data + }) + lead.insert(ignore_permissions=True) + return lead + +def get_customer_or_lead(client_name): + if check_if_customer(client_name): + return frappe.get_doc("Customer", client_name) + else: + lead_name = frappe.db.get_all("Lead", pluck="name", filters={"lead_name": client_name})[0] + return frappe.get_doc("Lead", lead_name) \ No newline at end of file diff --git a/custom_ui/api/db/contacts.py b/custom_ui/api/db/contacts.py new file mode 100644 index 0000000..629caeb --- /dev/null +++ b/custom_ui/api/db/contacts.py @@ -0,0 +1,50 @@ +import frappe + +def existing_contact_name(first_name: str, last_name: str, email: str, phone: str) -> str: + """Check if a contact exists based on provided details.""" + filters = { + "first_name": first_name, + "last_name": last_name, + "email_id": email, + "phone": phone + } + existing_contacts = frappe.db.get_all("Contact", pluck="name", filters=filters) + return existing_contacts[0] if existing_contacts else None + +def get_contact(contact_name: str): + """Retrieve a contact document by name.""" + contact = frappe.get_doc("Contact", contact_name) + print("Retrieved existing contact:", contact.as_dict()) + return contact + +def check_and_get_contact(first_name: str, last_name: str, email: str, phone: str): + """Check if a contact exists and return the contact document if found.""" + contact_name = existing_contact_name(first_name, last_name, email, phone) + if contact_name: + return get_contact(contact_name) + return None + +def create_contact(contact_data: dict): + """Create a new contact.""" + contact = frappe.get_doc({ + "doctype": "Contact", + **contact_data + }) + contact.insert(ignore_permissions=True) + print("Created new contact:", contact.as_dict()) + return contact + +def create_contact_links(contact_docs, client_doc, address_doc): + print("#####DEBUG: Linking contacts to client and address.") + for contact_doc in contact_docs: + contact_doc.address = address_doc.name + contact_doc.append("links", { + "link_doctype": client_doc.doctype, + "link_name": client_doc.name + }) + contact_doc.append("links", { + "link_doctype": "Address", + "link_name": address_doc.name + }) + contact_doc.custom_customer = client_doc.name + contact_doc.save(ignore_permissions=True) \ No newline at end of file diff --git a/custom_ui/api/db/estimates.py b/custom_ui/api/db/estimates.py index 714cb8f..f39c191 100644 --- a/custom_ui/api/db/estimates.py +++ b/custom_ui/api/db/estimates.py @@ -2,6 +2,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 +from custom_ui.api.db.clients import check_if_customer, convert_lead_to_customer # =============================================================================== # ESTIMATES & INVOICES API METHODS @@ -152,6 +153,8 @@ def send_estimate_email(estimate_name): quotation.custom_current_status = "Submitted" quotation.custom_sent = 1 quotation.save() + quotation.submit() + frappe.db.commit() updated_quotation = frappe.get_doc("Quotation", estimate_name) return build_success_response(updated_quotation.as_dict()) except Exception as e: @@ -166,20 +169,26 @@ def update_response(name, response): if not frappe.db.exists("Quotation", name): raise Exception("Estimate not found.") estimate = frappe.get_doc("Quotation", name) + if estimate.docstatus != 1: + raise Exception("Estimate must be submitted to update response.") 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.status = "Ordered" if accepted else "Closed" estimate.flags.ignore_permissions = True print("DEBUG: Updating estimate with response:", response, "and status:", new_status) - # estimate.save() - estimate.submit() - frappe.db.commit() + estimate.save() if accepted: template = "custom_ui/templates/estimates/accepted.html" + if check_if_customer(estimate.party_name): + print("DEBUG: Party is already a customer:", estimate.party_name) + else: + print("DEBUG: Converting lead to customer for party:", estimate.party_name) + convert_lead_to_customer(estimate.party_name) elif response == "Requested call": template = "custom_ui/templates/estimates/request-call.html" else: @@ -211,6 +220,7 @@ def upsert_estimate(data): # Update fields estimate.custom_installation_address = data.get("address_name") estimate.party_name = data.get("contact_name") + estimate.custom_requires_half_payment = data.get("requires_half_payment", 0) # Clear existing items and add new ones estimate.items = [] diff --git a/custom_ui/api/db/invoices.py b/custom_ui/api/db/invoices.py index 65f7eac..4b7c3ef 100644 --- a/custom_ui/api/db/invoices.py +++ b/custom_ui/api/db/invoices.py @@ -102,3 +102,4 @@ def upsert_invoice(data): except Exception as e: return build_error_response(str(e), 500) + diff --git a/custom_ui/db_utils.py b/custom_ui/db_utils.py index 6e7ab96..2b32c67 100644 --- a/custom_ui/db_utils.py +++ b/custom_ui/db_utils.py @@ -159,6 +159,14 @@ def build_full_address(doc): return f"{first}, {second}" return first or second or "" +def build_address_title(customer_name, address_data): + title_parts = [customer_name] + if address_data.get("address_line1"): + title_parts.append(address_data["address_line1"]) + if address_data.get("type"): + title_parts.append(address_data["type"]) + return " - ".join(title_parts) + def map_lead_client(client_data): mappings = { "lead_name": "customer_name", @@ -170,7 +178,8 @@ def map_lead_client(client_data): if lead_field in client_data: print(f"DEBUG: Mapping field {lead_field} to {client_field} with value {client_data[lead_field]}") client_data[client_field] = client_data[lead_field] - client_data["customer_group"] = "" # Leads don't have customer groups + client_data["customer_group"] = "" + print("####DEBUG: Mapped client data:", client_data) return client_data def map_lead_update(client_data): diff --git a/custom_ui/events/estimate.py b/custom_ui/events/estimate.py index ae145f9..7123247 100644 --- a/custom_ui/events/estimate.py +++ b/custom_ui/events/estimate.py @@ -22,9 +22,10 @@ def after_save(doc, method): 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) +def on_update_after_submit(doc, method): + print("DEBUG: on_update_after_submit hook triggered for Quotation:", doc.name) 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.custom_installation_address) address_doc.custom_estimate_sent_status = "Completed" @@ -33,7 +34,8 @@ def after_submit(doc, method): new_sales_order = make_sales_order(doc.name) new_sales_order.custom_requires_half_payment = doc.requires_half_payment new_sales_order.insert() + new_sales_order.submit() 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") + frappe.log_error(f"Error creating Sales Order from Estimate {doc.name}: {str(e)}", "Estimate on_update_after_submit Error") \ No newline at end of file diff --git a/custom_ui/events/sales_order.py b/custom_ui/events/sales_order.py index 4bb71ff..662fd6a 100644 --- a/custom_ui/events/sales_order.py +++ b/custom_ui/events/sales_order.py @@ -3,4 +3,42 @@ import frappe def after_insert(doc, method): print(doc.as_dict()) # Create Invoice and Project from Sales Order - \ No newline at end of file + +def create_sales_invoice_from_sales_order(doc, method): + try: + print("DEBUG: after_submit hook triggered for Sales Order:", doc.name) + invoice_ammount = doc.grand_total / 2 if doc.requires_half_payment else doc.grand_total + items = [] + for so_item in doc.items: + # proportionally reduce rate if half-payment + rate = so_item.rate / 2 if doc.requires_half_payment else so_item.rate + qty = so_item.qty # usually full qty, but depends on half-payment rules + items.append({ + "item_code": so_item.item_code, + "qty": qty, + "rate": rate, + "income_account": so_item.income_account, + "cost_center": so_item.cost_center, + "so_detail": so_item.name # links item to Sales Order + }) + invoice = frappe.get_doc({ + "doctype": "Sales Invoice", + "customer": doc.customer, + "company": doc.company, + "posting_date": frappe.utils.nowdate(), + "due_date": frappe.utils.nowdate(), # or calculate from payment terms + "currency": doc.currency, + "update_stock": 0, + "items": items, + "sales_order": doc.name, # link invoice to Sales Order + "ignore_pricing_rule": 1, + "payment_schedule": doc.payment_schedule if not half_payment else [] # optional + }) + + invoice.insert() + invoice.submit() + frappe.db.commit() + return invoice + except Exception as e: + print("ERROR creating Sales Invoice from Sales Order:", str(e)) + frappe.log_error(f"Error creating Sales Invoice from Sales Order {doc.name}: {str(e)}", "Sales Order after_submit Error") \ No newline at end of file diff --git a/custom_ui/hooks.py b/custom_ui/hooks.py index 07112af..b2c2414 100644 --- a/custom_ui/hooks.py +++ b/custom_ui/hooks.py @@ -169,7 +169,8 @@ doc_events = { "Quotation": { "after_insert": "custom_ui.events.estimate.after_insert", "on_update": "custom_ui.events.estimate.after_save", - "after_submit": "custom_ui.events.estimate.after_submit" + "after_submit": "custom_ui.events.estimate.after_submit", + "on_update_after_submit": "custom_ui.events.estimate.on_update_after_submit" }, "Sales Order": { "after_insert": "custom_ui.events.sales_order.after_insert" diff --git a/frontend/src/components/clientSubPages/AddressInformationForm.vue b/frontend/src/components/clientSubPages/AddressInformationForm.vue index 77390cd..d7666c0 100644 --- a/frontend/src/components/clientSubPages/AddressInformationForm.vue +++ b/frontend/src/components/clientSubPages/AddressInformationForm.vue @@ -2,16 +2,6 @@