+ We look forward to working with you! +
diff --git a/Screenshot from 2026-02-04 12-41-31.png b/Screenshot from 2026-02-04 12-41-31.png new file mode 100644 index 0000000..6df5795 Binary files /dev/null and b/Screenshot from 2026-02-04 12-41-31.png differ diff --git a/custom_ui/api/db/estimates.py b/custom_ui/api/db/estimates.py index 8aadf5a..501b219 100644 --- a/custom_ui/api/db/estimates.py +++ b/custom_ui/api/db/estimates.py @@ -2,9 +2,9 @@ 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 DbUtils, 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, EstimateService, ItemService +from frappe.email.doctype.email_template.email_template import get_email_template # =============================================================================== # ESTIMATES & INVOICES API METHODS @@ -191,7 +191,7 @@ def send_estimate_email(estimate_name): print("DEBUG: Sending estimate email for:", estimate_name) quotation = frappe.get_doc("Quotation", estimate_name) - + # Get recipient email 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) @@ -210,21 +210,71 @@ def send_estimate_email(estimate_name): 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) + # Get customer name + customer_name = party.first_name or party.name or "Valued Customer" + if party.last_name: + customer_name = f"{party.first_name} {party.last_name}" + + # Get full address + full_address = "Address not specified" + if quotation.custom_job_address: + address_doc = frappe.get_doc("Address", quotation.custom_job_address) + full_address = address_doc.full_address or address_doc.address_line1 or "Address not specified" + + # Format price + price = frappe.utils.fmt_money(quotation.grand_total, currency=quotation.currency) + + # Get additional notes + additional = quotation.terms or "" + + # Get company phone + company_phone = "" + if quotation.company: + company_doc = frappe.get_doc("Company", quotation.company) + company_phone = getattr(company_doc, 'phone_no', '') or getattr(company_doc, 'phone', '') + + # Get base URL + base_url = frappe.utils.get_url() + + # Get letterhead image + letterhead_image = "" + if quotation.letter_head: + letterhead_doc = frappe.get_doc("Letter Head", quotation.letter_head) + if letterhead_doc.image: + letterhead_image = frappe.utils.get_url() + letterhead_doc.image + + # Prepare template context + template_context = { + "company": quotation.company, + "customer_name": customer_name, + "price": price, + "address": full_address, + "additional": additional, + "company_phone": company_phone, + "base_url": base_url, + "estimate_name": quotation.name, + "letterhead_image": letterhead_image + } + + # Render the email template + template_path = "custom_ui/templates/emails/general_estimation.html" + message = frappe.render_template(template_path, template_context) + subject = f"Estimate from {quotation.company} - {quotation.name}" + + print("DEBUG: Subject:", subject) + print("DEBUG: Sending email to:", email) + + # Generate PDF attachment 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.") + + # Send email frappe.sendmail( recipients=email, subject=subject, - content=message, + message=message, doctype="Quotation", name=quotation.name, read_receipt=1, @@ -232,11 +282,14 @@ def send_estimate_email(estimate_name): attachments=[{"fname": f"{quotation.name}.pdf", "fcontent": pdf}] ) print(f"DEBUG: Email sent to {email} successfully.") + + # Update quotation status 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: @@ -269,45 +322,6 @@ def manual_response(name, response): 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.""" @@ -462,6 +476,7 @@ def upsert_estimate(data): estimate.append("items", { "item_code": item.get("item_code"), "qty": item.get("qty"), + "rate": item.get("rate"), "discount_amount": item.get("discount_amount") or item.get("discountAmount", 0), "discount_percentage": item.get("discount_percentage") or item.get("discountPercentage", 0) }) @@ -506,6 +521,7 @@ def upsert_estimate(data): new_estimate.append("items", { "item_code": item.get("item_code"), "qty": item.get("qty"), + "rate": item.get("rate"), "discount_amount": item.get("discount_amount") or item.get("discountAmount", 0), "discount_percentage": item.get("discount_percentage") or item.get("discountPercentage", 0) }) diff --git a/custom_ui/api/public/estimates.py b/custom_ui/api/public/estimates.py new file mode 100644 index 0000000..3a3716a --- /dev/null +++ b/custom_ui/api/public/estimates.py @@ -0,0 +1,43 @@ +import frappe +from werkzeug.wrappers import Response + + +@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}) + frappe.db.commit() + 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") \ No newline at end of file diff --git a/custom_ui/api/public/payments.py b/custom_ui/api/public/payments.py index 9f4a88a..7461e6a 100644 --- a/custom_ui/api/public/payments.py +++ b/custom_ui/api/public/payments.py @@ -1,5 +1,6 @@ import frappe import json +from datetime import datetime from frappe.utils.data import flt from custom_ui.services import DbService, StripeService, PaymentService from custom_ui.models import PaymentData @@ -14,16 +15,17 @@ def half_down_stripe_payment(sales_order): frappe.throw("This sales order does not require a half-down payment.") if so.docstatus != 1: frappe.throw("Sales Order must be submitted to proceed with payment.") - if so.custom_halfdown_is_paid or so.advanced_paid >= so.custom_halfdown_amount: + if so.custom_halfdown_is_paid or so.advance_paid >= so.custom_halfdown_amount: frappe.throw("Half-down payment has already been made for this sales order.") stripe_session = StripeService.create_checkout_session( company=so.company, amount=so.custom_halfdown_amount, service=so.custom_project_template, - sales_order=so.name, + order_num=so.name, for_advance_payment=True ) - return frappe.redirect(stripe_session.url) + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = stripe_session.url @frappe.whitelist(allow_guest=True) def stripe_webhook(): @@ -31,7 +33,6 @@ def stripe_webhook(): payload = frappe.request.get_data() sig_header = frappe.request.headers.get('Stripe-Signature') session, metadata = StripeService.get_session_and_metadata(payload, sig_header) - required_keys = ["order_num", "company", "payment_type"] for key in required_keys: if not metadata.get(key): @@ -40,8 +41,6 @@ def stripe_webhook(): if DbService.exists("Payment Entry", {"reference_no": session.id}): raise frappe.ValidationError("Payment Entry already exists for this session.") - - reference_doctype = "Sales Invoice" if metadata.get("payment_type") == "advance": reference_doctype = "Sales Order" @@ -50,20 +49,30 @@ def stripe_webhook(): amount_paid = flt(session.amount_total) / 100 - # stripe_settings = StripeService.get_stripe_settings(metadata.get("company")) + # Convert Unix timestamp to date string (YYYY-MM-DD) + reference_date = datetime.fromtimestamp(session.created).strftime('%Y-%m-%d') - pe = PaymentService.create_payment_entry( - data=PaymentData( - mode_of_payment="Stripe", - reference_no=session.id, - reference_date=session.created, - received_amount=amount_paid, - company=metadata.get("company"), - reference_doc_name=metadata.get("order_num") + # Set Administrator context to create Payment Entry + frappe.set_user("Administrator") + + try: + pe = PaymentService.create_payment_entry( + data=PaymentData( + mode_of_payment="Stripe", + reference_no=session.id, + reference_date=reference_date, + received_amount=amount_paid, + company=metadata.get("company"), + reference_doc_name=metadata.get("order_num") + ) ) - ) - pe.submit() - return "Payment Entry created and submitted successfully." + pe.flags.ignore_permissions = True + pe.submit() + frappe.db.commit() + return "Payment Entry created and submitted successfully." + finally: + # Reset to Guest user + frappe.set_user("Guest") diff --git a/custom_ui/events/client.py b/custom_ui/events/client.py index e69de29..bf1a838 100644 --- a/custom_ui/events/client.py +++ b/custom_ui/events/client.py @@ -0,0 +1,6 @@ +import frappe + + +def before_save(doc, method): + print("DEBUG: Before save hook triggered for Customer:", doc.name) + print("DEBUG: current state: ", doc.as_dict()) \ No newline at end of file diff --git a/custom_ui/events/estimate.py b/custom_ui/events/estimate.py index d6465a1..ab5e78d 100644 --- a/custom_ui/events/estimate.py +++ b/custom_ui/events/estimate.py @@ -31,13 +31,13 @@ def before_insert(doc, method): print("DEBUG: CHECKING CUSTOMER NAME") print(doc.actual_customer_name) print("Quotation_to:", doc.quotation_to) - doc.customer_address = frappe.get_value(doc.customer_type, doc.actual_customer_name, "customer_billing_address") + doc.customer_address = frappe.get_value(doc.customer_type, doc.actual_customer_name, "custom_billing_address") # 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.") + # 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) diff --git a/custom_ui/events/sales_order.py b/custom_ui/events/sales_order.py index d8d659f..ed51bba 100644 --- a/custom_ui/events/sales_order.py +++ b/custom_ui/events/sales_order.py @@ -71,6 +71,79 @@ def after_insert(doc, method): ClientService.append_link_v2( doc.customer, "sales_orders", {"sales_order": doc.name, "project_template": doc.custom_project_template} ) + + # Send down payment email if required + if doc.requires_half_payment: + try: + print("DEBUG: Sales Order requires half payment, preparing to send down payment email") + + # Get the customer/lead document to find the billing contact + customer_doc = frappe.get_doc("Customer", doc.customer) + + # Get billing contact email + email = None + if hasattr(customer_doc, 'primary_contact') and customer_doc.primary_contact: + primary_contact = frappe.get_doc("Contact", customer_doc.primary_contact) + email = primary_contact.email_id + + # Fallback to primary contact or first available contact + if not email and hasattr(customer_doc, 'customer_primary_contact') and customer_doc.customer_primary_contact: + primary_contact = frappe.get_doc("Contact", customer_doc.customer_primary_contact) + email = primary_contact.email_id + + # Last resort - try to get any contact from the customer + if not email: + contact_links = frappe.get_all("Dynamic Link", + filters={ + "link_doctype": "Customer", + "link_name": doc.customer, + "parenttype": "Contact" + }, + pluck="parent" + ) + if contact_links: + contact = frappe.get_doc("Contact", contact_links[0]) + email = contact.email_id + + if not email: + print(f"ERROR: No email found for customer {doc.customer}, cannot send down payment email") + return + + # Prepare template context + half_down_amount = doc.custom_halfdown_amount or (doc.grand_total / 2) + base_url = frappe.utils.get_url() + + template_context = { + "company_name": doc.company, + "customer_name": doc.customer_name or doc.customer, + "sales_order_number": doc.name, + "total_amount": frappe.utils.fmt_money(half_down_amount, currency=doc.currency), + "base_url": base_url + } + + # Render the email template + template_path = "custom_ui/templates/emails/downpayment.html" + message = frappe.render_template(template_path, template_context) + subject = f"Down Payment Required - {doc.company} - {doc.name}" + + print(f"DEBUG: Sending down payment email to {email}") + + # Send email + frappe.sendmail( + recipients=email, + subject=subject, + message=message, + doctype="Sales Order", + name=doc.name + ) + + print(f"DEBUG: Down payment email sent to {email} successfully") + + except Exception as e: + print(f"ERROR: Failed to send down payment email: {str(e)}") + # Don't raise the exception - we don't want to block the sales order creation + frappe.log_error(f"Failed to send down payment email for {doc.name}: {str(e)}", "Down Payment Email Error") + def on_update_after_submit(doc, method): print("DEBUG: on_update_after_submit hook triggered for Sales Order:", doc.name) diff --git a/custom_ui/services/address_service.py b/custom_ui/services/address_service.py index 38bc14c..21eef35 100644 --- a/custom_ui/services/address_service.py +++ b/custom_ui/services/address_service.py @@ -186,6 +186,7 @@ class AddressService: address_doc.append(field, link) print("DEBUG: Saving address document after appending link.") address_doc.save(ignore_permissions=True) + frappe.db.commit() print(f"DEBUG: Set link field {field} for Address {address_name} with link data {link}") @staticmethod diff --git a/custom_ui/services/client_service.py b/custom_ui/services/client_service.py index 099b4d2..e633cec 100644 --- a/custom_ui/services/client_service.py +++ b/custom_ui/services/client_service.py @@ -55,6 +55,7 @@ class ClientService: client_doc.append(field, link) print("DEBUG: Saving client document after appending link.") client_doc.save(ignore_permissions=True) + frappe.db.commit() print(f"DEBUG: Set link field {field} for client {client_doc.get('name')} with link data {link}") @staticmethod @@ -91,6 +92,7 @@ class ClientService: try: print(f"DEBUG: Processing address: {address.get('address')}") ClientService.append_link_v2(customer_doc.name, "properties", {"address": address.get("address")}) + customer_doc.reload() 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") @@ -104,6 +106,7 @@ class ClientService: try: print(f"DEBUG: Processing contact: {contact.get('contact')}") ClientService.append_link_v2(customer_doc.name, "contacts", {"contact": contact.get("contact")}) + customer_doc.reload() 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") @@ -117,6 +120,7 @@ class ClientService: try: print(f"DEBUG: Processing quotation: {quotation.get('quotation')}") ClientService.append_link_v2(customer_doc.name, "quotations", {"quotation": quotation.get("quotation")}) + customer_doc.reload() 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") @@ -130,6 +134,7 @@ class ClientService: 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")}) + customer_doc.reload() 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: @@ -141,11 +146,13 @@ class ClientService: try: print(f"DEBUG: Processing company: {company.get('company')}") ClientService.append_link_v2(customer_doc.name, "companies", {"company": company.get("company")}) + customer_doc.reload() print(f"DEBUG: Linked company {company.get('company')} to customer") except Exception as e: print(f"ERROR: Failed to link company {company.get('company')}: {str(e)}") frappe.log_error(f"Company linking error: {str(e)}", "convert_lead_to_customer") print(f"DEBUG: Converted Lead {lead_name} to Customer {customer_doc.name}") + frappe.db.commit() return customer_doc except Exception as e: diff --git a/custom_ui/services/email_service.py b/custom_ui/services/email_service.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_ui/services/payment_service.py b/custom_ui/services/payment_service.py index db84f18..b54f83a 100644 --- a/custom_ui/services/payment_service.py +++ b/custom_ui/services/payment_service.py @@ -25,14 +25,15 @@ class PaymentService: "paid_to": account, "reference_no": data.reference_no, "reference_date": data.reference_date or frappe.utils.nowdate(), + "paid_amount": data.received_amount, "received_amount": data.received_amount, "paid_currency": "USD", "received_currency": "USD", - }).append("references", { - "reference_doctype": reference_doc.doctype, - "reference_name": reference_doc.name, - "reconcile_effect_on": reference_doc.doctype, - "allocated_amount": data.received_amount, + "references": [{ + "reference_doctype": reference_doc.doctype, + "reference_name": reference_doc.name, + "allocated_amount": data.received_amount, + }] }) pe.insert() print(f"DEBUG: Created Payment Entry with name: {pe.name}") diff --git a/custom_ui/services/stripe_service.py b/custom_ui/services/stripe_service.py index 7f02c57..ed3d2c7 100644 --- a/custom_ui/services/stripe_service.py +++ b/custom_ui/services/stripe_service.py @@ -19,7 +19,7 @@ class StripeService: def get_api_key(company: str) -> str: """Retrieve the Stripe API key for the specified company.""" settings = StripeService.get_stripe_settings(company) - return settings.secret_key + return settings.get_password("secret_key") @staticmethod def get_webhook_secret(company: str) -> str: @@ -62,20 +62,46 @@ class StripeService: @staticmethod def get_event(payload: bytes, sig_header: str, company: str = None) -> stripe.Event: - company = company if company else json.loads(payload).get("data", {}).get("object", {}).get("metadata", {}).get("company") + print("DEBUG: Stripe webhook received") + print(f"DEBUG: Signature header present: {bool(sig_header)}") + + # If company not provided, try to extract from payload metadata if not company: + try: + payload_dict = json.loads(payload) + print(f"DEBUG: Parsed payload type: {payload_dict.get('type')}") + + metadata = payload_dict.get("data", {}).get("object", {}).get("metadata", {}) + print(f"DEBUG: Metadata from payload: {metadata}") + + company = metadata.get("company") + print(f"DEBUG: Extracted company from metadata: {company}") + except (json.JSONDecodeError, KeyError, AttributeError) as e: + print(f"DEBUG: Failed to parse payload: {str(e)}") + + # If we still don't have a company, reject the webhook + if not company: + print("ERROR: Company information missing in webhook payload") frappe.throw("Company information missing in webhook payload.") + + print(f"DEBUG: Validating webhook signature for company: {company}") + + # Validate webhook signature with the specified company's secret try: event = stripe.Webhook.construct_event( payload=payload, sig_header=sig_header, - secret=StripeService.get_webhook_secret(company), - api_key=StripeService.get_api_key(company) + secret=StripeService.get_webhook_secret(company) ) + print(f"DEBUG: Webhook signature validated successfully for company: {company}") + print(f"DEBUG: Event type: {event.type}") + print(f"DEBUG: Event ID: {event.id}") except ValueError as e: + print(f"ERROR: Invalid payload: {str(e)}") frappe.throw(f"Invalid payload: {str(e)}") except stripe.error.SignatureVerificationError as e: - frappe.throw(f"Invalid signature: {str(e)}") + print(f"ERROR: Invalid signature for company {company}: {str(e)}") + frappe.throw(f"Invalid signature for company {company}: {str(e)}") return event diff --git a/custom_ui/templates/email/downpayment.html b/custom_ui/templates/emails/downpayment.html similarity index 94% rename from custom_ui/templates/email/downpayment.html rename to custom_ui/templates/emails/downpayment.html index 136424c..d9ce712 100644 --- a/custom_ui/templates/email/downpayment.html +++ b/custom_ui/templates/emails/downpayment.html @@ -78,7 +78,7 @@
Down Payment Amount: ${{ total_amount }}
Please click the button below to make your secure payment through our payment processor:
- Make Payment + Make PaymentIf you have any questions or need assistance, feel free to contact us. We're here to help!
Best regards,
The Team at {{ company_name }}