From 02c48e6108cca6044b6aa46190a1b3cac14e516a Mon Sep 17 00:00:00 2001 From: Casey Date: Mon, 8 Dec 2025 16:58:16 -0600 Subject: [PATCH] update doctype workflows and events for Sales Order creation from Quotation --- custom_ui/api/db/estimates.py | 131 ++++++++++++++++------------- custom_ui/api/db/sales_orders.py | 17 ++++ custom_ui/events/estimate.py | 5 +- custom_ui/events/onsite_meeting.py | 13 ++- custom_ui/events/quotation.py | 10 --- custom_ui/hooks.py | 2 +- custom_ui/install.py | 9 ++ 7 files changed, 106 insertions(+), 81 deletions(-) create mode 100644 custom_ui/api/db/sales_orders.py delete mode 100644 custom_ui/events/quotation.py diff --git a/custom_ui/api/db/estimates.py b/custom_ui/api/db/estimates.py index 7070680..32294c9 100644 --- a/custom_ui/api/db/estimates.py +++ b/custom_ui/api/db/estimates.py @@ -104,56 +104,76 @@ def get_estimate_from_address(full_address): @frappe.whitelist() def send_estimate_email(estimate_name): # def send_estimate_email_job(estimate_name): - print("DEBUG: Sending estimate email for:", estimate_name) - quotation = frappe.get_doc("Quotation", estimate_name) + try: + 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: + return build_error_response("No email found for the customer.", 400) + party = frappe.get_doc(quotation.quotation_to, quotation.party_name) + + 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 + + if not email and quotation.custom_installation_address: + address = frappe.get_doc("Address", quotation.custom_installation_address) + email = getattr(address, 'email_id', None) + if not email: + return build_error_response("No email found for the customer or address.", 400) + + # email = "casey@shilohcode.com" + template_name = "Quote with Actions - SNW" + template = frappe.get_doc("Email Template", template_name) + message = frappe.render_template(template.response, {"name": quotation.name}) + subject = frappe.render_template(template.subject, {"doc": quotation}) + print("DEBUG: Message: ", message) + print("DEBUG: Subject: ", subject) + html = frappe.get_print("Quotation", quotation.name, print_format="Quotation - SNW - Standard", letterhead=True) + print("DEBUG: Generated HTML for PDF.") + pdf = get_pdf(html) + print("DEBUG: Generated PDF for email attachment.") + frappe.sendmail( + recipients=email, + subject=subject, + content=message, + doctype="Quotation", + name=quotation.name, + read_receipt=1, + print_letterhead=1, + attachments=[{"fname": f"{quotation.name}.pdf", "fcontent": pdf}] + ) + print(f"DEBUG: Email sent to {email} successfully.") + quotation.custom_current_status = "Submitted" + 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)}") + return build_error_response(str(e), 500) + +@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" - party_exists = frappe.db.exists(quotation.quotation_to, quotation.party_name) - if not party_exists: - return build_error_response("No email found for the customer.", 400) - party = frappe.get_doc(quotation.quotation_to, quotation.party_name) - - 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 - - if not email and quotation.custom_installation_address: - address = frappe.get_doc("Address", quotation.custom_installation_address) - email = getattr(address, 'email_id', None) - if not email: - return build_error_response("No email found for the customer or address.", 400) - - # email = "casey@shilohcode.com" - template_name = "Quote with Actions - SNW" - template = frappe.get_doc("Email Template", template_name) - message = frappe.render_template(template.response, {"name": quotation.name}) - subject = frappe.render_template(template.subject, {"doc": quotation}) - print("DEBUG: Message: ", message) - print("DEBUG: Subject: ", subject) - html = frappe.get_print("Quotation", quotation.name, print_format="Quotation - SNW - Standard", letterhead=True) - print("DEBUG: Generated HTML for PDF.") - pdf = get_pdf(html) - print("DEBUG: Generated PDF for email attachment.") - frappe.sendmail( - recipients=email, - subject=subject, - content=message, - doctype="Quotation", - name=quotation.name, - read_receipt=1, - print_letterhead=1, - attachments=[{"fname": f"{quotation.name}.pdf", "fcontent": pdf}] - ) - print(f"DEBUG: Email sent to {email} successfully.") - quotation.custom_current_status = "Submitted" - quotation.custom_sent = 1 - quotation.save() - quotation.submit() - updated_quotation = frappe.get_doc("Quotation", estimate_name) - print("DEBUG: Quotation submitted successfully.") - return build_success_response(updated_quotation.as_dict()) + 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() + + + @frappe.whitelist() def upsert_estimate(data): @@ -193,6 +213,7 @@ def upsert_estimate(data): new_estimate = frappe.get_doc({ "doctype": "Quotation", "custom_installation_address": data.get("address_name"), + "custom_current_status": "Draft", "contact_email": data.get("contact_email"), "party_name": data.get("contact_name"), "company": "Sprinklers Northwest", @@ -210,14 +231,4 @@ def upsert_estimate(data): except Exception as e: print(f"DEBUG: Error in upsert_estimate: {str(e)}") return build_error_response(str(e), 500) - -@frappe.whitelist() -def lock_estimate(estimate_name): - """Lock an estimate to prevent further edits.""" - try: - estimate = frappe.get_doc("Quotation", estimate_name) - estimate.submit() - final_estimate = frappe.get_doc("Quotation", estimate_name) - return build_success_response(final_estimate.as_dict()) - except Exception as e: - return build_error_response(str(e), 500) \ No newline at end of file + \ No newline at end of file diff --git a/custom_ui/api/db/sales_orders.py b/custom_ui/api/db/sales_orders.py new file mode 100644 index 0000000..5415098 --- /dev/null +++ b/custom_ui/api/db/sales_orders.py @@ -0,0 +1,17 @@ +import frappe +from custom_ui.db_utils import build_success_response, build_error_response +from erpnext.selling.doctype.quotation.quotation import make_sales_order + +@frappe.whitelist() +def create_sales_order_from_estimate(estimate_name): + """Create a Sales Order from a given Estimate (Quotation).""" + try: + estimate = frappe.get_doc("Quotation", estimate_name) + if estimate.custom_current_status != "Estimate Accepted": + raise Exception("Estimate must be accepted to create a Sales Order.") + new_sales_order = make_sales_order(estimate_name) + new_sales_order.custom_requires_half_payment = estimate.requires_half_payment + new_sales_order.insert() + return build_success_response(new_sales_order.as_dict()) + except Exception as e: + return build_error_response(str(e), 500) \ No newline at end of file diff --git a/custom_ui/events/estimate.py b/custom_ui/events/estimate.py index 0d9bcae..affb1dd 100644 --- a/custom_ui/events/estimate.py +++ b/custom_ui/events/estimate.py @@ -1,4 +1,5 @@ import frappe +from erpnext.selling.doctype.quotation.quotation import make_sales_order def after_insert(doc, method): try: @@ -14,9 +15,9 @@ 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: + 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() \ No newline at end of file + address_doc.save() diff --git a/custom_ui/events/onsite_meeting.py b/custom_ui/events/onsite_meeting.py index 0bb316d..eb4d03b 100644 --- a/custom_ui/events/onsite_meeting.py +++ b/custom_ui/events/onsite_meeting.py @@ -7,14 +7,11 @@ def after_insert(doc, method): address_doc = frappe.get_doc("Address", doc.address) address_doc.custom_onsite_meeting_scheduled = "In Progress" address_doc.save() - if doc.status == "Completed": - address_doc = frappe.get_doc("Address", doc.address) - address_doc.custom_onsite_meeting_scheduled = "Completed" - address_doc.save() - -def on_update(doc, method): - print("DEBUG: On Update Triggered for On-Site Meeting") + +def after_save(doc, method): + print("DEBUG: After Save Triggered for On-Site Meeting") if doc.status == "Completed": print("DEBUG: Meeting marked as Completed, updating Address status") address_doc = frappe.get_doc("Address", doc.address) - address_doc.custom_onsite_meeting_scheduled = "Completed" \ No newline at end of file + address_doc.custom_onsite_meeting_scheduled = "Completed" + address_doc.save() \ No newline at end of file diff --git a/custom_ui/events/quotation.py b/custom_ui/events/quotation.py deleted file mode 100644 index cf99998..0000000 --- a/custom_ui/events/quotation.py +++ /dev/null @@ -1,10 +0,0 @@ -import frappe - -def after_insert(doc, method): - address_title = doc.custom_installation_address - address_name = frappe.db.get_value("Address", fieldname="name", filters={"address_title": address_title}) - if address_name: - address_doc = frappe.get_doc("Address", address_name) - address_doc.custom_estimate_sent_status = "In Progress" - address_doc.save() - diff --git a/custom_ui/hooks.py b/custom_ui/hooks.py index dc2bfae..470be07 100644 --- a/custom_ui/hooks.py +++ b/custom_ui/hooks.py @@ -161,7 +161,7 @@ add_to_apps_screen = [ doc_events = { "On-Site Meeting": { "after_insert": "custom_ui.events.onsite_meeting.after_insert", - "on_update": "custom_ui.events.onsite_meeting.on_update" + "after_save": "custom_ui.events.onsite_meeting.after_save" }, "Address": { "after_insert": "custom_ui.events.address.after_insert" diff --git a/custom_ui/install.py b/custom_ui/install.py index 57dba1d..32d7f8f 100644 --- a/custom_ui/install.py +++ b/custom_ui/install.py @@ -169,6 +169,15 @@ def add_custom_fields(): options="Employee", insert_after="status" ) + ], + "Quotation": [ + dict( + fieldname="requires_half_payment", + label="Requires Half Payment", + fieldtype="Check", + default=0, + insert_after="custom_installation_address" + ) ] }