fix bugs and stripe
This commit is contained in:
parent
9a7e3fe740
commit
21a256a26f
17 changed files with 542 additions and 88 deletions
BIN
Screenshot from 2026-02-04 12-41-31.png
Normal file
BIN
Screenshot from 2026-02-04 12-41-31.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 496 KiB |
|
|
@ -2,9 +2,9 @@ import frappe, json
|
||||||
from frappe.utils.pdf import get_pdf
|
from frappe.utils.pdf import get_pdf
|
||||||
from custom_ui.api.db.general import get_doc_history
|
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 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.api.db.clients import check_if_customer, convert_lead_to_customer
|
||||||
from custom_ui.services import DbService, ClientService, AddressService, ContactService, EstimateService, ItemService
|
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
|
# ESTIMATES & INVOICES API METHODS
|
||||||
|
|
@ -191,7 +191,7 @@ def send_estimate_email(estimate_name):
|
||||||
print("DEBUG: Sending estimate email for:", estimate_name)
|
print("DEBUG: Sending estimate email for:", estimate_name)
|
||||||
quotation = frappe.get_doc("Quotation", estimate_name)
|
quotation = frappe.get_doc("Quotation", estimate_name)
|
||||||
|
|
||||||
|
# Get recipient email
|
||||||
if not DbService.exists("Contact", quotation.contact_person):
|
if not DbService.exists("Contact", quotation.contact_person):
|
||||||
return build_error_response("No email found for the customer.", 400)
|
return build_error_response("No email found for the customer.", 400)
|
||||||
party = ContactService.get_or_throw(quotation.contact_person)
|
party = ContactService.get_or_throw(quotation.contact_person)
|
||||||
|
|
@ -210,21 +210,71 @@ def send_estimate_email(estimate_name):
|
||||||
if not email:
|
if not email:
|
||||||
return build_error_response("No email found for the customer or address.", 400)
|
return build_error_response("No email found for the customer or address.", 400)
|
||||||
|
|
||||||
# email = "casey@shilohcode.com"
|
# Get customer name
|
||||||
template_name = "Quote with Actions - SNW"
|
customer_name = party.first_name or party.name or "Valued Customer"
|
||||||
template = frappe.get_doc("Email Template", template_name)
|
if party.last_name:
|
||||||
message = frappe.render_template(template.response, {"name": quotation.name})
|
customer_name = f"{party.first_name} {party.last_name}"
|
||||||
subject = frappe.render_template(template.subject, {"doc": quotation})
|
|
||||||
print("DEBUG: Message: ", message)
|
# Get full address
|
||||||
print("DEBUG: Subject: ", subject)
|
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)
|
html = frappe.get_print("Quotation", quotation.name, print_format="Quotation - SNW - Standard", letterhead=True)
|
||||||
print("DEBUG: Generated HTML for PDF.")
|
print("DEBUG: Generated HTML for PDF.")
|
||||||
pdf = get_pdf(html)
|
pdf = get_pdf(html)
|
||||||
print("DEBUG: Generated PDF for email attachment.")
|
print("DEBUG: Generated PDF for email attachment.")
|
||||||
|
|
||||||
|
# Send email
|
||||||
frappe.sendmail(
|
frappe.sendmail(
|
||||||
recipients=email,
|
recipients=email,
|
||||||
subject=subject,
|
subject=subject,
|
||||||
content=message,
|
message=message,
|
||||||
doctype="Quotation",
|
doctype="Quotation",
|
||||||
name=quotation.name,
|
name=quotation.name,
|
||||||
read_receipt=1,
|
read_receipt=1,
|
||||||
|
|
@ -232,11 +282,14 @@ def send_estimate_email(estimate_name):
|
||||||
attachments=[{"fname": f"{quotation.name}.pdf", "fcontent": pdf}]
|
attachments=[{"fname": f"{quotation.name}.pdf", "fcontent": pdf}]
|
||||||
)
|
)
|
||||||
print(f"DEBUG: Email sent to {email} successfully.")
|
print(f"DEBUG: Email sent to {email} successfully.")
|
||||||
|
|
||||||
|
# Update quotation status
|
||||||
quotation.custom_current_status = "Submitted"
|
quotation.custom_current_status = "Submitted"
|
||||||
quotation.custom_sent = 1
|
quotation.custom_sent = 1
|
||||||
quotation.save()
|
quotation.save()
|
||||||
quotation.submit()
|
quotation.submit()
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
updated_quotation = frappe.get_doc("Quotation", estimate_name)
|
updated_quotation = frappe.get_doc("Quotation", estimate_name)
|
||||||
return build_success_response(updated_quotation.as_dict())
|
return build_success_response(updated_quotation.as_dict())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -269,45 +322,6 @@ def manual_response(name, response):
|
||||||
return build_error_response(str(e), 500)
|
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()
|
@frappe.whitelist()
|
||||||
def get_estimate_templates(company):
|
def get_estimate_templates(company):
|
||||||
"""Get available estimate templates."""
|
"""Get available estimate templates."""
|
||||||
|
|
@ -462,6 +476,7 @@ def upsert_estimate(data):
|
||||||
estimate.append("items", {
|
estimate.append("items", {
|
||||||
"item_code": item.get("item_code"),
|
"item_code": item.get("item_code"),
|
||||||
"qty": item.get("qty"),
|
"qty": item.get("qty"),
|
||||||
|
"rate": item.get("rate"),
|
||||||
"discount_amount": item.get("discount_amount") or item.get("discountAmount", 0),
|
"discount_amount": item.get("discount_amount") or item.get("discountAmount", 0),
|
||||||
"discount_percentage": item.get("discount_percentage") or item.get("discountPercentage", 0)
|
"discount_percentage": item.get("discount_percentage") or item.get("discountPercentage", 0)
|
||||||
})
|
})
|
||||||
|
|
@ -506,6 +521,7 @@ def upsert_estimate(data):
|
||||||
new_estimate.append("items", {
|
new_estimate.append("items", {
|
||||||
"item_code": item.get("item_code"),
|
"item_code": item.get("item_code"),
|
||||||
"qty": item.get("qty"),
|
"qty": item.get("qty"),
|
||||||
|
"rate": item.get("rate"),
|
||||||
"discount_amount": item.get("discount_amount") or item.get("discountAmount", 0),
|
"discount_amount": item.get("discount_amount") or item.get("discountAmount", 0),
|
||||||
"discount_percentage": item.get("discount_percentage") or item.get("discountPercentage", 0)
|
"discount_percentage": item.get("discount_percentage") or item.get("discountPercentage", 0)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
43
custom_ui/api/public/estimates.py
Normal file
43
custom_ui/api/public/estimates.py
Normal file
|
|
@ -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")
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import frappe
|
import frappe
|
||||||
import json
|
import json
|
||||||
|
from datetime import datetime
|
||||||
from frappe.utils.data import flt
|
from frappe.utils.data import flt
|
||||||
from custom_ui.services import DbService, StripeService, PaymentService
|
from custom_ui.services import DbService, StripeService, PaymentService
|
||||||
from custom_ui.models import PaymentData
|
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.")
|
frappe.throw("This sales order does not require a half-down payment.")
|
||||||
if so.docstatus != 1:
|
if so.docstatus != 1:
|
||||||
frappe.throw("Sales Order must be submitted to proceed with payment.")
|
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.")
|
frappe.throw("Half-down payment has already been made for this sales order.")
|
||||||
stripe_session = StripeService.create_checkout_session(
|
stripe_session = StripeService.create_checkout_session(
|
||||||
company=so.company,
|
company=so.company,
|
||||||
amount=so.custom_halfdown_amount,
|
amount=so.custom_halfdown_amount,
|
||||||
service=so.custom_project_template,
|
service=so.custom_project_template,
|
||||||
sales_order=so.name,
|
order_num=so.name,
|
||||||
for_advance_payment=True
|
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)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def stripe_webhook():
|
def stripe_webhook():
|
||||||
|
|
@ -31,7 +33,6 @@ def stripe_webhook():
|
||||||
payload = frappe.request.get_data()
|
payload = frappe.request.get_data()
|
||||||
sig_header = frappe.request.headers.get('Stripe-Signature')
|
sig_header = frappe.request.headers.get('Stripe-Signature')
|
||||||
session, metadata = StripeService.get_session_and_metadata(payload, sig_header)
|
session, metadata = StripeService.get_session_and_metadata(payload, sig_header)
|
||||||
|
|
||||||
required_keys = ["order_num", "company", "payment_type"]
|
required_keys = ["order_num", "company", "payment_type"]
|
||||||
for key in required_keys:
|
for key in required_keys:
|
||||||
if not metadata.get(key):
|
if not metadata.get(key):
|
||||||
|
|
@ -40,8 +41,6 @@ def stripe_webhook():
|
||||||
if DbService.exists("Payment Entry", {"reference_no": session.id}):
|
if DbService.exists("Payment Entry", {"reference_no": session.id}):
|
||||||
raise frappe.ValidationError("Payment Entry already exists for this session.")
|
raise frappe.ValidationError("Payment Entry already exists for this session.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
reference_doctype = "Sales Invoice"
|
reference_doctype = "Sales Invoice"
|
||||||
if metadata.get("payment_type") == "advance":
|
if metadata.get("payment_type") == "advance":
|
||||||
reference_doctype = "Sales Order"
|
reference_doctype = "Sales Order"
|
||||||
|
|
@ -50,20 +49,30 @@ def stripe_webhook():
|
||||||
|
|
||||||
amount_paid = flt(session.amount_total) / 100
|
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')
|
||||||
|
|
||||||
|
# Set Administrator context to create Payment Entry
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
|
try:
|
||||||
pe = PaymentService.create_payment_entry(
|
pe = PaymentService.create_payment_entry(
|
||||||
data=PaymentData(
|
data=PaymentData(
|
||||||
mode_of_payment="Stripe",
|
mode_of_payment="Stripe",
|
||||||
reference_no=session.id,
|
reference_no=session.id,
|
||||||
reference_date=session.created,
|
reference_date=reference_date,
|
||||||
received_amount=amount_paid,
|
received_amount=amount_paid,
|
||||||
company=metadata.get("company"),
|
company=metadata.get("company"),
|
||||||
reference_doc_name=metadata.get("order_num")
|
reference_doc_name=metadata.get("order_num")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
pe.flags.ignore_permissions = True
|
||||||
pe.submit()
|
pe.submit()
|
||||||
|
frappe.db.commit()
|
||||||
return "Payment Entry created and submitted successfully."
|
return "Payment Entry created and submitted successfully."
|
||||||
|
finally:
|
||||||
|
# Reset to Guest user
|
||||||
|
frappe.set_user("Guest")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
@ -31,13 +31,13 @@ def before_insert(doc, method):
|
||||||
print("DEBUG: CHECKING CUSTOMER NAME")
|
print("DEBUG: CHECKING CUSTOMER NAME")
|
||||||
print(doc.actual_customer_name)
|
print(doc.actual_customer_name)
|
||||||
print("Quotation_to:", doc.quotation_to)
|
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)
|
# print("Party_type:", doc.party_type)
|
||||||
if doc.custom_project_template == "SNW Install":
|
if doc.custom_project_template == "SNW Install":
|
||||||
print("DEBUG: Quotation uses SNW Install template, making sure no duplicate linked estimates.")
|
print("DEBUG: Quotation uses SNW Install template, making sure no duplicate linked estimates.")
|
||||||
address_doc = AddressService.get_or_throw(doc.custom_job_address)
|
address_doc = AddressService.get_or_throw(doc.custom_job_address)
|
||||||
if "SNW Install" in [link.project_template for link in address_doc.quotations]:
|
# 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.")
|
# raise frappe.ValidationError("An Estimate with project template 'SNW Install' is already linked to this address.")
|
||||||
|
|
||||||
def before_submit(doc, method):
|
def before_submit(doc, method):
|
||||||
print("DEBUG: Before submit hook triggered for Quotation:", doc.name)
|
print("DEBUG: Before submit hook triggered for Quotation:", doc.name)
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,79 @@ def after_insert(doc, method):
|
||||||
doc.customer, "sales_orders", {"sales_order": doc.name, "project_template": doc.custom_project_template}
|
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):
|
def on_update_after_submit(doc, method):
|
||||||
print("DEBUG: on_update_after_submit hook triggered for Sales Order:", doc.name)
|
print("DEBUG: on_update_after_submit hook triggered for Sales Order:", doc.name)
|
||||||
if doc.requires_half_payment and doc.custom_halfdown_is_paid:
|
if doc.requires_half_payment and doc.custom_halfdown_is_paid:
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,7 @@ class AddressService:
|
||||||
address_doc.append(field, link)
|
address_doc.append(field, link)
|
||||||
print("DEBUG: Saving address document after appending link.")
|
print("DEBUG: Saving address document after appending link.")
|
||||||
address_doc.save(ignore_permissions=True)
|
address_doc.save(ignore_permissions=True)
|
||||||
|
frappe.db.commit()
|
||||||
print(f"DEBUG: Set link field {field} for Address {address_name} with link data {link}")
|
print(f"DEBUG: Set link field {field} for Address {address_name} with link data {link}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ class ClientService:
|
||||||
client_doc.append(field, link)
|
client_doc.append(field, link)
|
||||||
print("DEBUG: Saving client document after appending link.")
|
print("DEBUG: Saving client document after appending link.")
|
||||||
client_doc.save(ignore_permissions=True)
|
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}")
|
print(f"DEBUG: Set link field {field} for client {client_doc.get('name')} with link data {link}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -91,6 +92,7 @@ class ClientService:
|
||||||
try:
|
try:
|
||||||
print(f"DEBUG: Processing address: {address.get('address')}")
|
print(f"DEBUG: Processing address: {address.get('address')}")
|
||||||
ClientService.append_link_v2(customer_doc.name, "properties", {"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"))
|
address_doc = AddressService.get_or_throw(address.get("address"))
|
||||||
AddressService.link_address_to_customer(address_doc, "Customer", customer_doc.name)
|
AddressService.link_address_to_customer(address_doc, "Customer", customer_doc.name)
|
||||||
print(f"DEBUG: Linked address {address.get('address')} to customer")
|
print(f"DEBUG: Linked address {address.get('address')} to customer")
|
||||||
|
|
@ -104,6 +106,7 @@ class ClientService:
|
||||||
try:
|
try:
|
||||||
print(f"DEBUG: Processing contact: {contact.get('contact')}")
|
print(f"DEBUG: Processing contact: {contact.get('contact')}")
|
||||||
ClientService.append_link_v2(customer_doc.name, "contacts", {"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"))
|
contact_doc = ContactService.get_or_throw(contact.get("contact"))
|
||||||
ContactService.link_contact_to_customer(contact_doc, "Customer", customer_doc.name)
|
ContactService.link_contact_to_customer(contact_doc, "Customer", customer_doc.name)
|
||||||
print(f"DEBUG: Linked contact {contact.get('contact')} to customer")
|
print(f"DEBUG: Linked contact {contact.get('contact')} to customer")
|
||||||
|
|
@ -117,6 +120,7 @@ class ClientService:
|
||||||
try:
|
try:
|
||||||
print(f"DEBUG: Processing quotation: {quotation.get('quotation')}")
|
print(f"DEBUG: Processing quotation: {quotation.get('quotation')}")
|
||||||
ClientService.append_link_v2(customer_doc.name, "quotations", {"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"))
|
quotation_doc = EstimateService.get_or_throw(quotation.get("quotation"))
|
||||||
EstimateService.link_estimate_to_customer(quotation_doc, "Customer", customer_doc.name)
|
EstimateService.link_estimate_to_customer(quotation_doc, "Customer", customer_doc.name)
|
||||||
print(f"DEBUG: Linked quotation {quotation.get('quotation')} to customer")
|
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')}")
|
print(f"DEBUG: Processing onsite meeting: {meeting.get('onsite_meeting')}")
|
||||||
meeting_doc = DbService.get_or_throw("On-Site 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")})
|
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)
|
OnSiteMeetingService.link_onsite_meeting_to_customer(meeting_doc, "Customer", customer_doc.name)
|
||||||
print(f"DEBUG: Linked onsite meeting {meeting.get('onsite_meeting')} to customer")
|
print(f"DEBUG: Linked onsite meeting {meeting.get('onsite_meeting')} to customer")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -141,11 +146,13 @@ class ClientService:
|
||||||
try:
|
try:
|
||||||
print(f"DEBUG: Processing company: {company.get('company')}")
|
print(f"DEBUG: Processing company: {company.get('company')}")
|
||||||
ClientService.append_link_v2(customer_doc.name, "companies", {"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")
|
print(f"DEBUG: Linked company {company.get('company')} to customer")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"ERROR: Failed to link company {company.get('company')}: {str(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")
|
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}")
|
print(f"DEBUG: Converted Lead {lead_name} to Customer {customer_doc.name}")
|
||||||
|
frappe.db.commit()
|
||||||
return customer_doc
|
return customer_doc
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
0
custom_ui/services/email_service.py
Normal file
0
custom_ui/services/email_service.py
Normal file
|
|
@ -25,14 +25,15 @@ class PaymentService:
|
||||||
"paid_to": account,
|
"paid_to": account,
|
||||||
"reference_no": data.reference_no,
|
"reference_no": data.reference_no,
|
||||||
"reference_date": data.reference_date or frappe.utils.nowdate(),
|
"reference_date": data.reference_date or frappe.utils.nowdate(),
|
||||||
|
"paid_amount": data.received_amount,
|
||||||
"received_amount": data.received_amount,
|
"received_amount": data.received_amount,
|
||||||
"paid_currency": "USD",
|
"paid_currency": "USD",
|
||||||
"received_currency": "USD",
|
"received_currency": "USD",
|
||||||
}).append("references", {
|
"references": [{
|
||||||
"reference_doctype": reference_doc.doctype,
|
"reference_doctype": reference_doc.doctype,
|
||||||
"reference_name": reference_doc.name,
|
"reference_name": reference_doc.name,
|
||||||
"reconcile_effect_on": reference_doc.doctype,
|
|
||||||
"allocated_amount": data.received_amount,
|
"allocated_amount": data.received_amount,
|
||||||
|
}]
|
||||||
})
|
})
|
||||||
pe.insert()
|
pe.insert()
|
||||||
print(f"DEBUG: Created Payment Entry with name: {pe.name}")
|
print(f"DEBUG: Created Payment Entry with name: {pe.name}")
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ class StripeService:
|
||||||
def get_api_key(company: str) -> str:
|
def get_api_key(company: str) -> str:
|
||||||
"""Retrieve the Stripe API key for the specified company."""
|
"""Retrieve the Stripe API key for the specified company."""
|
||||||
settings = StripeService.get_stripe_settings(company)
|
settings = StripeService.get_stripe_settings(company)
|
||||||
return settings.secret_key
|
return settings.get_password("secret_key")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_webhook_secret(company: str) -> str:
|
def get_webhook_secret(company: str) -> str:
|
||||||
|
|
@ -62,20 +62,46 @@ class StripeService:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_event(payload: bytes, sig_header: str, company: str = None) -> stripe.Event:
|
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:
|
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.")
|
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:
|
try:
|
||||||
event = stripe.Webhook.construct_event(
|
event = stripe.Webhook.construct_event(
|
||||||
payload=payload,
|
payload=payload,
|
||||||
sig_header=sig_header,
|
sig_header=sig_header,
|
||||||
secret=StripeService.get_webhook_secret(company),
|
secret=StripeService.get_webhook_secret(company)
|
||||||
api_key=StripeService.get_api_key(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:
|
except ValueError as e:
|
||||||
|
print(f"ERROR: Invalid payload: {str(e)}")
|
||||||
frappe.throw(f"Invalid payload: {str(e)}")
|
frappe.throw(f"Invalid payload: {str(e)}")
|
||||||
except stripe.error.SignatureVerificationError as 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
|
return event
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@
|
||||||
<p><strong>Down Payment Amount:</strong> ${{ total_amount }}</p>
|
<p><strong>Down Payment Amount:</strong> ${{ total_amount }}</p>
|
||||||
</div>
|
</div>
|
||||||
<p>Please click the button below to make your secure payment through our payment processor:</p>
|
<p>Please click the button below to make your secure payment through our payment processor:</p>
|
||||||
<a href="https://yourdomain.com/downpayment?so={{ sales_order_number }}&amount={{ total_amount }}" class="cta-button">Make Payment</a>
|
<a href="{{ base_url }}/api/method/custom_ui.api.public.payments.half_down_stripe_payment?sales_order={{ sales_order_number }}" class="cta-button">Make Payment</a>
|
||||||
<p>If you have any questions or need assistance, feel free to contact us. We're here to help!</p>
|
<p>If you have any questions or need assistance, feel free to contact us. We're here to help!</p>
|
||||||
<p>Best regards,<br>The Team at {{ company_name }}</p>
|
<p>Best regards,<br>The Team at {{ company_name }}</p>
|
||||||
</div>
|
</div>
|
||||||
251
custom_ui/templates/emails/general_estimation.html
Normal file
251
custom_ui/templates/emails/general_estimation.html
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Estimate from {{ company }}</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
.email-container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
.letterhead {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px 20px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-bottom: 3px solid #0066cc;
|
||||||
|
}
|
||||||
|
.letterhead img {
|
||||||
|
max-width: 250px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.company-name {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333333;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 40px 30px;
|
||||||
|
}
|
||||||
|
.greeting {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.intro-text {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #555555;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.estimate-box {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 25px;
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
.estimate-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666666;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.estimate-value {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.estimate-value:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.price-section {
|
||||||
|
background-color: #0066cc;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.price-label {
|
||||||
|
font-size: 14px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.price-amount {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.additional-section {
|
||||||
|
background-color: #fff9e6;
|
||||||
|
border-left: 4px solid #ffc107;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 30px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.additional-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #856404;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.additional-text {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
.action-buttons {
|
||||||
|
text-align: center;
|
||||||
|
margin: 40px 0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 14px 28px;
|
||||||
|
margin: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
.btn-accept {
|
||||||
|
background-color: #28a745;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.btn-decline {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.btn-call {
|
||||||
|
background-color: #ffc107;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
.closing-text {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #555555;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
.contact-info {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #0066cc;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
.footer-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666666;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
.content {
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
.estimate-box {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.price-amount {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
.company-name {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
display: block;
|
||||||
|
margin: 10px auto;
|
||||||
|
max-width: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="email-container">
|
||||||
|
<!-- Letterhead Section -->
|
||||||
|
<div class="letterhead">
|
||||||
|
{% if letterhead_image %}
|
||||||
|
<img src="{{ letterhead_image }}" alt="{{ company }} Logo">
|
||||||
|
{% else %}
|
||||||
|
<div class="company-name">{{ company }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="content">
|
||||||
|
<div class="greeting">Hello {{ customer_name }},</div>
|
||||||
|
|
||||||
|
<div class="intro-text">
|
||||||
|
Thank you for considering {{ company }} for your project. We are pleased to provide you with the following estimate for the services requested.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Estimate Details Box -->
|
||||||
|
<div class="estimate-box">
|
||||||
|
<div class="estimate-label">Service Location</div>
|
||||||
|
<div class="estimate-value">{{ address }}</div>
|
||||||
|
|
||||||
|
<!-- Price Section -->
|
||||||
|
<div class="price-section">
|
||||||
|
<div class="price-label">Total Estimate</div>
|
||||||
|
<div class="price-amount">{{ price }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Additional Notes (Conditional) -->
|
||||||
|
{% if additional %}
|
||||||
|
<div class="additional-section">
|
||||||
|
<div class="additional-label">Additional Notes</div>
|
||||||
|
<div class="additional-text">{{ additional }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="action-buttons">
|
||||||
|
<a href="{{ base_url }}/api/method/custom_ui.api.public.estimates.update_response?name={{ estimate_name }}&response=Accepted" class="btn btn-accept">Accept</a>
|
||||||
|
<a href="{{ base_url }}/api/method/custom_ui.api.public.estimates.update_response?name={{ estimate_name }}&response=Rejected" class="btn btn-decline">Decline</a>
|
||||||
|
<a href="{{ base_url }}/api/method/custom_ui.api.public.estimates.update_response?name={{ estimate_name }}&response=Requested%20call" class="btn btn-call">Request a Call</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Closing -->
|
||||||
|
<div class="closing-text">
|
||||||
|
This estimate is valid for 30 days from the date of this email. If you have any questions or would like to proceed with this estimate, please don't hesitate to contact us.
|
||||||
|
{% if company_phone %}
|
||||||
|
<div class="contact-info">Call us at: {{ company_phone }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<br>
|
||||||
|
We look forward to working with you!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="footer">
|
||||||
|
<div class="footer-text">
|
||||||
|
<strong>{{ company }}</strong><br>
|
||||||
|
This is an automated message. Please do not reply directly to this email.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
0
custom_ui/templates/emails/snw_install_estimation.html
Normal file
0
custom_ui/templates/emails/snw_install_estimation.html
Normal file
|
|
@ -318,9 +318,9 @@ const handleSubmit = async () => {
|
||||||
const createdClient = await Api.createClient(client.value);
|
const createdClient = await Api.createClient(client.value);
|
||||||
console.log("Created client:", createdClient);
|
console.log("Created client:", createdClient);
|
||||||
notificationStore.addSuccess("Client created successfully!");
|
notificationStore.addSuccess("Client created successfully!");
|
||||||
stripped_name = createdClient.customerName.split("-#-")[0].trim();
|
const strippedName = createdClient.name.split("-#-")[0].trim();
|
||||||
// Navigate to the created client
|
// Navigate to the created client
|
||||||
router.push('/client?client=' + encodeURIComponent(stripped_name));
|
router.push('/client?client=' + encodeURIComponent(strippedName));
|
||||||
} else {
|
} else {
|
||||||
// TODO: Implement save logic
|
// TODO: Implement save logic
|
||||||
notificationStore.addSuccess("Changes saved successfully!");
|
notificationStore.addSuccess("Changes saved successfully!");
|
||||||
|
|
|
||||||
|
|
@ -926,6 +926,11 @@ watch(
|
||||||
|
|
||||||
formData.projectTemplate = estimate.value.customProjectTemplate || estimate.value.custom_project_template || null;
|
formData.projectTemplate = estimate.value.customProjectTemplate || estimate.value.custom_project_template || null;
|
||||||
|
|
||||||
|
// Load quotation items if project template is set (needed for item details)
|
||||||
|
if (formData.projectTemplate) {
|
||||||
|
quotationItems.value = await Api.getQuotationItems(formData.projectTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
if (estimate.value.items && estimate.value.items.length > 0) {
|
if (estimate.value.items && estimate.value.items.length > 0) {
|
||||||
selectedItems.value = estimate.value.items.map(item => {
|
selectedItems.value = estimate.value.items.map(item => {
|
||||||
const fullItem = quotationItems.value.find(qi => qi.itemCode === item.itemCode);
|
const fullItem = quotationItems.value.find(qi => qi.itemCode === item.itemCode);
|
||||||
|
|
@ -967,6 +972,10 @@ watch(
|
||||||
try {
|
try {
|
||||||
bidMeeting.value = await Api.getBidMeeting(newFromMeetingQuery);
|
bidMeeting.value = await Api.getBidMeeting(newFromMeetingQuery);
|
||||||
if (bidMeeting.value?.bidNotes?.quantities) {
|
if (bidMeeting.value?.bidNotes?.quantities) {
|
||||||
|
// Ensure quotationItems is an array before using find
|
||||||
|
if (!Array.isArray(quotationItems.value)) {
|
||||||
|
quotationItems.value = [];
|
||||||
|
}
|
||||||
selectedItems.value = bidMeeting.value.bidNotes.quantities.map(q => {
|
selectedItems.value = bidMeeting.value.bidNotes.quantities.map(q => {
|
||||||
const item = quotationItems.value.find(i => i.itemCode === q.item);
|
const item = quotationItems.value.find(i => i.itemCode === q.item);
|
||||||
return {
|
return {
|
||||||
|
|
@ -1014,6 +1023,10 @@ onMounted(async () => {
|
||||||
bidMeeting.value = await Api.getBidMeeting(fromMeetingQuery.value);
|
bidMeeting.value = await Api.getBidMeeting(fromMeetingQuery.value);
|
||||||
// If new estimate and bid notes have quantities, set default items
|
// If new estimate and bid notes have quantities, set default items
|
||||||
if (isNew.value && bidMeeting.value?.bidNotes?.quantities) {
|
if (isNew.value && bidMeeting.value?.bidNotes?.quantities) {
|
||||||
|
// Ensure quotationItems is an array before using find
|
||||||
|
if (!Array.isArray(quotationItems.value)) {
|
||||||
|
quotationItems.value = [];
|
||||||
|
}
|
||||||
selectedItems.value = bidMeeting.value.bidNotes.quantities.map(q => {
|
selectedItems.value = bidMeeting.value.bidNotes.quantities.map(q => {
|
||||||
const item = quotationItems.value.find(i => i.itemCode === q.item);
|
const item = quotationItems.value.find(i => i.itemCode === q.item);
|
||||||
return {
|
return {
|
||||||
|
|
@ -1069,18 +1082,26 @@ onMounted(async () => {
|
||||||
|
|
||||||
formData.projectTemplate = estimate.value.customProjectTemplate || estimate.value.custom_project_template || null;
|
formData.projectTemplate = estimate.value.customProjectTemplate || estimate.value.custom_project_template || null;
|
||||||
|
|
||||||
|
// Load quotation items if project template is set (needed for item details)
|
||||||
|
if (formData.projectTemplate) {
|
||||||
|
quotationItems.value = await Api.getQuotationItems(formData.projectTemplate);
|
||||||
|
}
|
||||||
|
// Ensure quotationItems is an array
|
||||||
|
if (!Array.isArray(quotationItems.value)) {
|
||||||
|
quotationItems.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
// Populate items from the estimate
|
// Populate items from the estimate
|
||||||
if (estimate.value.items && estimate.value.items.length > 0) {
|
if (estimate.value.items && estimate.value.items.length > 0) {
|
||||||
selectedItems.value = estimate.value.items.map(item => {
|
selectedItems.value = estimate.value.items.map(item => {
|
||||||
// Find the full item details from quotationItems
|
|
||||||
const fullItem = quotationItems.value.find(qi => qi.itemCode === item.itemCode);
|
|
||||||
const discountPercentage = item.discountPercentage || item.discount_percentage || 0;
|
const discountPercentage = item.discountPercentage || item.discount_percentage || 0;
|
||||||
const discountAmount = item.discountAmount || item.discount_amount || 0;
|
const discountAmount = item.discountAmount || item.discount_amount || 0;
|
||||||
return {
|
return {
|
||||||
itemCode: item.itemCode,
|
itemCode: item.itemCode || item.item_code,
|
||||||
itemName: item.itemName,
|
itemName: item.itemName || item.item_name,
|
||||||
qty: item.qty,
|
qty: item.qty,
|
||||||
standardRate: item.rate || fullItem?.standardRate || 0,
|
rate: item.rate,
|
||||||
|
standardRate: item.rate,
|
||||||
discountAmount: discountAmount === 0 ? null : discountAmount,
|
discountAmount: discountAmount === 0 ? null : discountAmount,
|
||||||
discountPercentage: discountPercentage === 0 ? null : discountPercentage,
|
discountPercentage: discountPercentage === 0 ? null : discountPercentage,
|
||||||
discountType: discountPercentage > 0 ? 'percentage' : 'currency'
|
discountType: discountPercentage > 0 ? 'percentage' : 'currency'
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue