import frappe, json from frappe.utils.pdf import get_pdf 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, ContactService # =============================================================================== # ESTIMATES & INVOICES API METHODS # =============================================================================== @frappe.whitelist() def get_estimate_table_data(filters={}, sortings=[], page=1, page_size=10): """Get paginated estimate table data with filtering and sorting support.""" print("DEBUG: Raw estimate options received:", filters, sortings, page, page_size) processed_filters, processed_sortings, is_or, page, page_size = process_query_conditions(filters, sortings, page, page_size) if is_or: count = frappe.db.sql(*get_count_or_filters("Quotation", processed_filters))[0][0] else: count = frappe.db.count("Quotation", filters=processed_filters) print(f"DEBUG: Number of estimates returned: {count}") estimates = frappe.db.get_all( "Quotation", fields=["*"], filters=processed_filters if not is_or else None, or_filters=processed_filters if is_or else None, limit=page_size, start=page * page_size, order_by=processed_sortings ) tableRows = [] for estimate in estimates: full_address = frappe.db.get_value("Address", estimate.get("custom_job_address"), "full_address") tableRow = {} tableRow["id"] = estimate["name"] tableRow["address"] = full_address tableRow["quotation_to"] = estimate.get("quotation_to", "") tableRow["customer"] = estimate.get("party_name", "") tableRow["status"] = estimate.get("custom_current_status", "") tableRow["date"] = estimate.get("transaction_date", "") tableRow["order_type"] = estimate.get("order_type", "") tableRow["items"] = estimate.get("items", "") tableRows.append(tableRow) table_data_dict = build_datatable_dict(data=tableRows, count=count, page=page, page_size=page_size) return build_success_response(table_data_dict) @frappe.whitelist() def get_quotation_items(): """Get all available quotation items.""" try: items = frappe.get_all("Item", fields=["*"], filters={"item_group": "SNW-S"}) return build_success_response(items) except Exception as e: return build_error_response(str(e), 500) @frappe.whitelist() def get_estimate(estimate_name): """Get detailed information for a specific estimate.""" try: estimate = frappe.get_doc("Quotation", estimate_name) est_dict = estimate.as_dict() address_name = estimate.custom_job_address or estimate.customer_address if address_name: # Fetch Address Doc address_doc = frappe.get_doc("Address", address_name).as_dict() est_dict["full_address"] = address_doc.get("full_address") # Logic from get_address_by_full_address to populate customer and contacts customer_exists = frappe.db.exists("Customer", address_doc.get("custom_customer_to_bill")) doctype = "Customer" if customer_exists else "Lead" name = "" if doctype == "Customer": name = address_doc.get("custom_customer_to_bill") else: lead_links = address_doc.get("links", []) lead_name = [link.link_name for link in lead_links if link.link_doctype == "Lead"] name = lead_name[0] if lead_name else "" if name: address_doc["customer"] = frappe.get_doc(doctype, name).as_dict() contacts = [] if address_doc.get("custom_linked_contacts"): for contact_link in address_doc.get("custom_linked_contacts"): contact_doc = frappe.get_doc("Contact", contact_link.contact) contacts.append(contact_doc.as_dict()) address_doc["contacts"] = contacts est_dict["address_details"] = address_doc est_dict["history"] = get_doc_history("Quotation", estimate_name) return build_success_response(est_dict) except Exception as e: return build_error_response(str(e), 500) @frappe.whitelist() def get_estimate_items(): items = frappe.db.get_all("Quotation Item", fields=["*"]) return build_success_response(items) @frappe.whitelist() def get_estimate_from_address(full_address): address_name = frappe.db.get_value("Address", {"full_address": full_address}, "name") quotation_name = frappe.db.get_value("Quotation", {"custom_job_address": address_name}, "name") quotation_doc = frappe.get_doc("Quotation", quotation_name) return build_success_response(quotation_doc.as_dict()) # quotation = frappe.db.sql(""" # SELECT q.name, q.custom_job_address # FROM `tabQuotation` q # JOIN `tabAddress` a # ON q.custom_job_address = a.name # WHERE a.full_address =%s # """, (full_address,), as_dict=True) # if quotation: # return build_success_response(quotation) # else: # return build_error_response("No quotation found for the given address.", 404) # @frappe.whitelist() # def send_estimate_email(estimate_name): # print("DEBUG: Queuing email send job for estimate:", estimate_name) # frappe.enqueue( # "custom_ui.api.db.estimates.send_estimate_email_job", # estimate_name=estimate_name, # queue="long", # or "default" # timeout=600, # ) # return build_success_response("Email queued for sending.") @frappe.whitelist() def send_estimate_email(estimate_name): # def send_estimate_email_job(estimate_name): try: print("DEBUG: Sending estimate email for:", estimate_name) quotation = frappe.get_doc("Quotation", estimate_name) if not DbService.exists("Contact", quotation.contact_person): return build_error_response("No email found for the customer.", 400) party = ContactService.get_or_throw(quotation.contact_person) 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) 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() frappe.db.commit() updated_quotation = frappe.get_doc("Quotation", estimate_name) 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() def manual_response(name, response): """Update the response for an estimate in the UI.""" 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) 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_current_status = new_status # 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() return build_success_response(estimate.as_dict()) except Exception as 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.""" 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) 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() 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: 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": str(e)}) return Response(html, mimetype="text/html") @frappe.whitelist() def get_estimate_templates(company): """Get available estimate templates.""" filters = {"is_active": 1} if company: filters["company"] = company try: print("DEBUG: Fetching estimate templates for company:", company) templates = frappe.get_all("Quotation Template", fields=["*"], filters=filters) result = [] if not templates: print("DEBUG: No templates found.") return build_success_response(result) print(f"DEBUG: Found {len(templates)} templates.") for template in templates: print("DEBUG: Processing template:", template) items = frappe.get_all("Quotation Template Item", fields=["item_code", "item_name", "description", "quantity", "discount_percentage", "rate"], filters={"parent": template.name}, order_by="idx") # Map fields to camelCase as requested mapped_items = [] for item in items: mapped_items.append({ "item_code": item.item_code, "item_name": item.item_name, "description": item.description, "quantity": item.quantity, "discount_percentage": item.discount_percentage, "rate": item.rate }) result.append({ "name": template.name, "template_name": template.template_name, "active": template.is_active, "description": template.description, "items": mapped_items, "project_template": template.project_template, }) return build_success_response(result) except Exception as e: return build_error_response(str(e), 500) @frappe.whitelist() def create_estimate_template(data): """Create a new estimate template.""" try: print("DEBUG: Creating estimate template with data:", data) data = json.loads(data) if isinstance(data, str) else data doc_data = { "doctype": "Quotation Template", "is_active": 1, "description": data.get("description"), "company": data.get("company"), "items": [], "template_name": data.get("template_name"), "custom_project_template": data.get("project_template", ""), "source_quotation": data.get("source_quotation", "") } new_template = frappe.get_doc(doc_data) for item in data.get("items", []): new_template.append("items", { "item_code": item.get("item_code"), "item_name": item.get("item_name"), "description": item.get("description"), "qty": item.get("qty") or item.get("quantity"), "rate": item.get("standard_rate") or item.get("rate"), "discount_percentage": item.get("discount_percentage") }) new_template.insert() return build_success_response(new_template.name) except Exception as e: return build_error_response(str(e), 500) # @frappe.whitelist() # def create_template(data): # """Create a new estimate template.""" # try: # data = json.loads(data) if isinstance(data, str) else data # print("DEBUG: Creating estimate template with data:", data) # new_template = frappe.get_doc({ # "doctype": "Quotation Template", # "template_name": data.get("templateName"), # "is_active": data.get("active", 1), # "description": data.get("description", ""), # "company": data.get("company", ""), # "source_quotation": data.get("source_quotation", "") # }) # for item in data.get("items", []): # item = json.loads(item) if isinstance(item, str) else item # new_template.append("items", { # "item_code": item.get("itemCode"), # "item_name": item.get("itemName"), # "description": item.get("description"), # "qty": item.get("quantity"), # "discount_percentage": item.get("discountPercentage"), # "rate": item.get("rate") # }) # new_template.insert() # print("DEBUG: New estimate template created with name:", new_template.name) # return build_success_response(new_template.as_dict()) # except Exception as e: # return build_error_response(str(e), 500) @frappe.whitelist() def upsert_estimate(data): """Create or update an estimate.""" 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") project_template = data.get("project_template", None) # If estimate_name exists, update existing estimate if estimate_name: print(f"DEBUG: Updating existing estimate: {estimate_name}") estimate = frappe.get_doc("Quotation", estimate_name) # Update fields # estimate.custom_installation_address = data.get("address") # estimate.custom_job_address = data.get("address_name") # estimate.party_name = data.get("customer") # estimate.contact_person = data.get("contact_name") estimate.custom_requires_half_payment = data.get("requires_half_payment", 0) estimate.custom_project_template = project_template estimate.custom_quotation_template = data.get("quotation_template", None) # estimate.company = data.get("company") # estimate.contact_email = data.get("contact_email") # estimate.quotation_to = client_doctype # estimate.customer_name = data.get("customer") # estimate.customer_address = data.get("address_name") # estimate.letter_head = data.get("company") # estimate.from_onsite_meeting = data.get("onsite_meeting", None) # Clear existing items and add new ones estimate.items = [] for item in data.get("items", []): item = json.loads(item) if isinstance(item, str) else item estimate.append("items", { "item_code": item.get("item_code"), "qty": item.get("qty"), "discount_amount": item.get("discount_amount") or item.get("discountAmount", 0), "discount_percentage": item.get("discount_percentage") or item.get("discountPercentage", 0) }) estimate.save() estimate_dict = estimate.as_dict() estimate_dict["history"] = get_doc_history("Quotation", estimate_name) print(f"DEBUG: Estimate updated: {estimate.name}") return build_success_response(estimate_dict) # Otherwise, create new estimate else: print("DEBUG: Creating new estimate") print("DEBUG: Retrieved address name:", data.get("address_name")) client_doc = ClientService.get_client_or_throw(address_doc.customer_name) # billing_address = next((addr for addr in address_doc if addr.address_type == "Billing"), None) # if billing_address: # print("DEBUG: Found billing address:", billing_address.name) # else: # print("DEBUG: No billing address found for client:", client_doc.name) 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("contact_name"), "quotation_to": "Contact", "company": data.get("company"), "actual_customer_name": client_doc.name, "customer_type": address_doc.customer_type, "customer_address": client_doc.custom_billing_address, "contact_person": data.get("contact_name"), "letter_head": data.get("company"), "custom_project_template": data.get("project_template", None), "custom_quotation_template": data.get("quotation_template", None), "from_onsite_meeting": data.get("onsite_meeting", None) }) for item in data.get("items", []): item = json.loads(item) if isinstance(item, str) else item new_estimate.append("items", { "item_code": item.get("item_code"), "qty": item.get("qty"), "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) print("DEBUG: New estimate created with name:", new_estimate.name) return build_success_response(new_estimate.as_dict()) except Exception as e: print(f"DEBUG: Error in upsert_estimate: {str(e)}") return build_error_response(str(e), 500) def get_estimate_history(estimate_name): """Get the history of changes for a specific estimate.""" pass # return history # @frappe.whitelist() # def get_estimate_counts(): # """Get specific counts of estimates based on their status.""" # try: # counts = { # "total_estimates": frappe.db.count("Quotation"), # "ready_to_" # }