lots of updates
This commit is contained in:
parent
02c48e6108
commit
8ed083fce1
14 changed files with 730 additions and 83 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import frappe, json
|
||||
from frappe.utils.pdf import get_pdf
|
||||
from custom_ui.db_utils import process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
# ===============================================================================
|
||||
# ESTIMATES & INVOICES API METHODS
|
||||
|
|
@ -152,7 +153,6 @@ def send_estimate_email(estimate_name):
|
|||
quotation.custom_sent = 1
|
||||
quotation.save()
|
||||
updated_quotation = frappe.get_doc("Quotation", estimate_name)
|
||||
print("DEBUG: Quotation submitted successfully.")
|
||||
return build_success_response(updated_quotation.as_dict())
|
||||
except Exception as e:
|
||||
print(f"DEBUG: Error in send_estimate_email: {str(e)}")
|
||||
|
|
@ -161,16 +161,35 @@ def send_estimate_email(estimate_name):
|
|||
@frappe.whitelist(allow_guest=True)
|
||||
def update_response(name, response):
|
||||
"""Update the response for a given estimate."""
|
||||
estimate = frappe.get_doc("Quotation", name)
|
||||
accepted = True if response == "Accepted" else False
|
||||
new_status = "Estimate Accepted" if accepted else "Lost"
|
||||
|
||||
estimate.custom_response = response
|
||||
estimate.custom_current_status = new_status
|
||||
estimate.custom_followup_needed = 1 if response == "Requested call" else 0
|
||||
estimate.flags.ignore_permissions = True
|
||||
estimate.save()
|
||||
frappe.db.commit()
|
||||
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)
|
||||
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.flags.ignore_permissions = True
|
||||
print("DEBUG: Updating estimate with response:", response, "and status:", new_status)
|
||||
# estimate.save()
|
||||
estimate.submit()
|
||||
frappe.db.commit()
|
||||
|
||||
if accepted:
|
||||
template = "custom_ui/templates/estimates/accepted.html"
|
||||
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_message": str(e)})
|
||||
return Response(html, mimetype="text/html")
|
||||
|
||||
|
||||
|
||||
|
|
@ -212,6 +231,7 @@ def upsert_estimate(data):
|
|||
print("DEBUG: Retrieved address name:", data.get("address_name"))
|
||||
new_estimate = frappe.get_doc({
|
||||
"doctype": "Quotation",
|
||||
"custom_requires_half_payment": data.get("requires_half_payment", 0),
|
||||
"custom_installation_address": data.get("address_name"),
|
||||
"custom_current_status": "Draft",
|
||||
"contact_email": data.get("contact_email"),
|
||||
|
|
|
|||
|
|
@ -118,9 +118,16 @@ def update_onsite_meeting(name, data):
|
|||
try:
|
||||
if isinstance(data, str):
|
||||
data = json.loads(data)
|
||||
data = {**defualts, **data}
|
||||
|
||||
# Ensure we always have the expected keys so fields can be cleared
|
||||
data = {**defualts, **(data or {})}
|
||||
meeting = frappe.get_doc("On-Site Meeting", name)
|
||||
for key, value in data.items():
|
||||
# Allow explicitly clearing date/time and assignment fields
|
||||
if key in ["start_time", "end_time", "assigned_employee", "completed_by"] and value is None:
|
||||
meeting.set(key, None)
|
||||
continue
|
||||
|
||||
if value is not None:
|
||||
if key == "address":
|
||||
value = frappe.db.get_value("Address", {"full_address": value}, "name")
|
||||
|
|
@ -128,6 +135,7 @@ def update_onsite_meeting(name, data):
|
|||
value = frappe.db.get_value("Employee", {"employee_name": value}, "name")
|
||||
meeting.set(key, value)
|
||||
meeting.save()
|
||||
frappe.db.commit()
|
||||
return build_success_response(meeting.as_dict())
|
||||
except frappe.DoesNotExistError:
|
||||
return build_error_response(f"On-Site Meeting '{name}' does not exist.", 404)
|
||||
|
|
|
|||
|
|
@ -15,9 +15,25 @@ def after_insert(doc, method):
|
|||
frappe.log_error(f"Error in estimate after_insert: {str(e)}", "Estimate Hook Error")
|
||||
|
||||
def after_save(doc, method):
|
||||
if not doc.custom_sent or not doc.custom_response:
|
||||
return
|
||||
print("DEBUG: Quotation has been sent, updating Address status")
|
||||
address_doc = frappe.get_doc("Address", doc.custom_installation_address)
|
||||
address_doc.custom_estimate_sent_status = "Completed"
|
||||
address_doc.save()
|
||||
print("DEBUG: after_save hook triggered for Quotation:", doc.name)
|
||||
if doc.custom_sent and doc.custom_response:
|
||||
print("DEBUG: Quotation has been sent, updating Address status")
|
||||
address_doc = frappe.get_doc("Address", doc.custom_installation_address)
|
||||
address_doc.custom_estimate_sent_status = "Completed"
|
||||
address_doc.save()
|
||||
|
||||
def after_submit(doc, method):
|
||||
print("DEBUG: on_submit hook triggered for Quotation:", doc.name)
|
||||
if doc.custom_current_status == "Estimate Accepted":
|
||||
print("DEBUG: Creating Sales Order from accepted Estimate")
|
||||
address_doc = frappe.get_doc("Address", doc.custom_installation_address)
|
||||
address_doc.custom_estimate_sent_status = "Completed"
|
||||
address_doc.save()
|
||||
try:
|
||||
new_sales_order = make_sales_order(doc.name)
|
||||
new_sales_order.custom_requires_half_payment = doc.requires_half_payment
|
||||
new_sales_order.insert()
|
||||
print("DEBUG: Sales Order created successfully:", new_sales_order.name)
|
||||
except Exception as e:
|
||||
print("ERROR creating Sales Order from Estimate:", str(e))
|
||||
frappe.log_error(f"Error creating Sales Order from Estimate {doc.name}: {str(e)}", "Estimate on_submit Error")
|
||||
|
|
|
|||
|
|
@ -14,4 +14,8 @@ def after_save(doc, method):
|
|||
print("DEBUG: Meeting marked as Completed, updating Address status")
|
||||
address_doc = frappe.get_doc("Address", doc.address)
|
||||
address_doc.custom_onsite_meeting_scheduled = "Completed"
|
||||
address_doc.save()
|
||||
address_doc.save()
|
||||
return
|
||||
if doc.status != "Scheduled" and doc.start_time and doc.end_time:
|
||||
doc.status = "Scheduled"
|
||||
doc.save()
|
||||
6
custom_ui/events/sales_order.py
Normal file
6
custom_ui/events/sales_order.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
|
||||
def after_insert(doc, method):
|
||||
print(doc.as_dict())
|
||||
# Create Invoice and Project from Sales Order
|
||||
|
||||
|
|
@ -161,14 +161,18 @@ add_to_apps_screen = [
|
|||
doc_events = {
|
||||
"On-Site Meeting": {
|
||||
"after_insert": "custom_ui.events.onsite_meeting.after_insert",
|
||||
"after_save": "custom_ui.events.onsite_meeting.after_save"
|
||||
"on_update": "custom_ui.events.onsite_meeting.after_save"
|
||||
},
|
||||
"Address": {
|
||||
"after_insert": "custom_ui.events.address.after_insert"
|
||||
},
|
||||
"Quotation": {
|
||||
"after_insert": "custom_ui.events.estimate.after_insert",
|
||||
"after_save": "custom_ui.events.estimate.after_save"
|
||||
"on_update": "custom_ui.events.estimate.after_save",
|
||||
"after_submit": "custom_ui.events.estimate.after_submit"
|
||||
},
|
||||
"Sales Order": {
|
||||
"after_insert": "custom_ui.events.sales_order.after_insert"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -178,6 +178,15 @@ def add_custom_fields():
|
|||
default=0,
|
||||
insert_after="custom_installation_address"
|
||||
)
|
||||
],
|
||||
"Sales Order": [
|
||||
dict(
|
||||
fieldname="requires_half_payment",
|
||||
label="Requires Half Payment",
|
||||
fieldtype="Check",
|
||||
default=0,
|
||||
insert_after="custom_installation_address"
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -239,29 +248,55 @@ def update_address_fields():
|
|||
|
||||
print(f"\n📍 Updating fields for {total_addresses} addresses...")
|
||||
|
||||
# Verify custom fields exist by checking the meta
|
||||
address_meta = frappe.get_meta("Address")
|
||||
required_fields = ['full_address', 'custom_onsite_meeting_scheduled',
|
||||
'custom_estimate_sent_status', 'custom_job_status',
|
||||
'custom_payment_received_status']
|
||||
|
||||
# Verify custom fields exist by checking the meta for every doctype that was customized
|
||||
def has_any_field(meta, candidates):
|
||||
return any(meta.has_field(f) for f in candidates)
|
||||
|
||||
custom_field_expectations = {
|
||||
"Address": [
|
||||
["full_address"],
|
||||
["custom_onsite_meeting_scheduled", "onsite_meeting_scheduled"],
|
||||
["custom_estimate_sent_status", "estimate_sent_status"],
|
||||
["custom_job_status", "job_status"],
|
||||
["custom_payment_received_status", "payment_received_status"]
|
||||
],
|
||||
"Contact": [
|
||||
["custom_role", "role"],
|
||||
["custom_email", "email"]
|
||||
],
|
||||
"On-Site Meeting": [
|
||||
["custom_notes", "notes"],
|
||||
["custom_assigned_employee", "assigned_employee"],
|
||||
["custom_status", "status"],
|
||||
["custom_completed_by", "completed_by"]
|
||||
],
|
||||
"Quotation": [
|
||||
["custom_requires_half_payment", "requires_half_payment"]
|
||||
],
|
||||
"Sales Order": [
|
||||
["custom_requires_half_payment", "requires_half_payment"]
|
||||
]
|
||||
}
|
||||
|
||||
missing_fields = []
|
||||
for field in required_fields:
|
||||
if not address_meta.has_field(field):
|
||||
missing_fields.append(field)
|
||||
|
||||
for doctype, field_options in custom_field_expectations.items():
|
||||
meta = frappe.get_meta(doctype)
|
||||
for candidates in field_options:
|
||||
if not has_any_field(meta, candidates):
|
||||
missing_fields.append(f"{doctype}: {'/'.join(candidates)}")
|
||||
|
||||
if missing_fields:
|
||||
print(f"\n❌ Missing custom fields: {', '.join(missing_fields)}")
|
||||
print("\n❌ Missing custom fields:")
|
||||
for entry in missing_fields:
|
||||
print(f" • {entry}")
|
||||
print(" Custom fields creation may have failed. Skipping address updates.")
|
||||
return
|
||||
|
||||
|
||||
print("✅ All custom fields verified. Proceeding with address updates...")
|
||||
|
||||
# Field update counters
|
||||
field_counters = {
|
||||
'full_address': 0,
|
||||
'latitude': 0,
|
||||
'longitude': 0,
|
||||
'custom_onsite_meeting_scheduled': 0,
|
||||
'custom_estimate_sent_status': 0,
|
||||
'custom_job_status': 0,
|
||||
|
|
@ -270,6 +305,9 @@ def update_address_fields():
|
|||
total_field_updates = 0
|
||||
addresses_updated = 0
|
||||
|
||||
onsite_meta = frappe.get_meta("On-Site Meeting")
|
||||
onsite_status_field = "custom_status" if onsite_meta.has_field("custom_status") else "status"
|
||||
|
||||
for index, name in enumerate(addresses, 1):
|
||||
# Calculate progress
|
||||
progress_percentage = int((index / total_addresses) * 100)
|
||||
|
|
@ -277,13 +315,25 @@ def update_address_fields():
|
|||
filled_length = int(bar_length * index // total_addresses)
|
||||
bar = '█' * filled_length + '░' * (bar_length - filled_length)
|
||||
|
||||
# Print progress bar with field update count
|
||||
print(f"\r📊 Progress: [{bar}] {progress_percentage:3d}% ({index}/{total_addresses}) | Fields Updated: {total_field_updates} - Processing: {name[:25]}...", end='', flush=True)
|
||||
|
||||
# Print a three-line, refreshing progress block to avoid terminal wrap
|
||||
progress_line = f"📊 Progress: [{bar}] {progress_percentage:3d}% ({index}/{total_addresses})"
|
||||
counters_line = f" Fields updated: {total_field_updates} | Addresses updated: {addresses_updated}"
|
||||
detail_line = f" Processing: {name[:40]}..."
|
||||
|
||||
if index == 1:
|
||||
# Save cursor position at the start of the progress block
|
||||
print("\033[s", end='')
|
||||
else:
|
||||
# Restore to the saved cursor position to rewrite the three-line block
|
||||
print("\033[u", end='')
|
||||
|
||||
print(f"\r\033[K{progress_line}")
|
||||
print(f"\r\033[K{counters_line}")
|
||||
print(f"\r\033[K{detail_line}", end='' if index != total_addresses else '\n', flush=True)
|
||||
|
||||
should_update = False
|
||||
address = frappe.get_doc("Address", name)
|
||||
current_address_updates = 0
|
||||
current_address_updates = 0
|
||||
|
||||
# Use getattr with default values instead of direct attribute access
|
||||
if not getattr(address, 'full_address', None):
|
||||
|
|
@ -310,14 +360,15 @@ def update_address_fields():
|
|||
job_status = "Not Started"
|
||||
payment_received = "Not Started"
|
||||
|
||||
onsite_meetings = frappe.get_all("On-Site Meeting", fields=["docstatus"],filters={"address": address.address_title})
|
||||
onsite_meetings = frappe.get_all("On-Site Meeting", fields=[onsite_status_field], filters={"address": address.address_title})
|
||||
if onsite_meetings and onsite_meetings[0]:
|
||||
onsite_meeting = "Completed" if onsite_meetings[0]["docstatus"] == 1 else "In Progress"
|
||||
status_value = onsite_meetings[0].get(onsite_status_field)
|
||||
onsite_meeting = "Completed" if status_value == "Completed" else "In Progress"
|
||||
|
||||
estimates = frappe.get_all("Quotation", fields=["custom_sent", "docstatus"], filters={"custom_installation_address": address.address_title})
|
||||
if estimates and estimates[0] and estimates[0]["custom_sent"] == 1 and estimates[0]["docstatus"] == 1:
|
||||
estimates = frappe.get_all("Quotation", fields=["custom_sent", "docstatus", "custom_response"], filters={"custom_installation_address": address.address_title})
|
||||
if estimates and estimates[0] and estimates[0]["custom_sent"] == 1 and estimates[0]["custom_response"]:
|
||||
estimate_sent = "Completed"
|
||||
elif estimates and estimates[0] and estimates[0]["docstatus"] != 1:
|
||||
elif estimates and estimates[0] and not (estimates[0]["custom_sent"] == 1 and estimates[0]["custom_response"]):
|
||||
estimate_sent = "In Progress"
|
||||
|
||||
jobs = frappe.get_all("Project", fields=["status"], filters={"custom_installation_address": address.address_title, "project_template": "SNW Install"})
|
||||
|
|
@ -367,8 +418,6 @@ def update_address_fields():
|
|||
print(f" • Total field updates: {total_field_updates:,}")
|
||||
print(f"\n📝 Field-specific updates:")
|
||||
print(f" • Full Address: {field_counters['full_address']:,}")
|
||||
print(f" • Latitude: {field_counters['latitude']:,}")
|
||||
print(f" • Longitude: {field_counters['longitude']:,}")
|
||||
print(f" • On-Site Meeting Status: {field_counters['custom_onsite_meeting_scheduled']:,}")
|
||||
print(f" • Estimate Sent Status: {field_counters['custom_estimate_sent_status']:,}")
|
||||
print(f" • Job Status: {field_counters['custom_job_status']:,}")
|
||||
|
|
|
|||
86
custom_ui/templates/estimates/accepted.html
Normal file
86
custom_ui/templates/estimates/accepted.html
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Quotation Accepted</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background: linear-gradient(135deg, #e8f5e8 0%, #c8e6c9 100%);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
background-color: white;
|
||||
padding: 50px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4);
|
||||
opacity: 0.1;
|
||||
transform: rotate(45deg);
|
||||
z-index: -1;
|
||||
}
|
||||
h1 {
|
||||
color: #1976d2;
|
||||
margin-bottom: 20px;
|
||||
font-size: 2.5em;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.checkmark {
|
||||
font-size: 3em;
|
||||
color: #ff9800;
|
||||
margin-bottom: 20px;
|
||||
animation: bounce 1s ease-in-out;
|
||||
}
|
||||
p {
|
||||
color: #424242;
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.highlight {
|
||||
color: #2196f3;
|
||||
font-weight: 600;
|
||||
}
|
||||
@keyframes bounce {
|
||||
0%, 20%, 50%, 80%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
60% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="checkmark">✓</div>
|
||||
<h1>Thank You, {{ doc.party_name or doc.customer }}!</h1>
|
||||
<p>You <span class="highlight">accepted the Quote</span>! You will receive a payment link shortly.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
74
custom_ui/templates/estimates/error.html
Normal file
74
custom_ui/templates/estimates/error.html
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Error</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background: linear-gradient(135deg, #e8f5e8 0%, #c8e6c9 100%);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
background-color: white;
|
||||
padding: 50px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4);
|
||||
opacity: 0.1;
|
||||
transform: rotate(45deg);
|
||||
z-index: -1;
|
||||
}
|
||||
h1 {
|
||||
color: #1976d2;
|
||||
margin-bottom: 20px;
|
||||
font-size: 2.5em;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.icon {
|
||||
font-size: 3em;
|
||||
color: #ff9800;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
p {
|
||||
color: #424242;
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.highlight {
|
||||
color: #2196f3;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="icon">⚠️</div>
|
||||
<h1>Oops! Something went wrong.</h1>
|
||||
<p>We're sorry, but an error occurred. Please try again later or contact support if the problem persists.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
87
custom_ui/templates/estimates/rejected.html
Normal file
87
custom_ui/templates/estimates/rejected.html
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Quotation Rejected</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background: linear-gradient(135deg, #e8f5e8 0%, #c8e6c9 100%);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
background-color: white;
|
||||
padding: 50px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4);
|
||||
opacity: 0.1;
|
||||
transform: rotate(45deg);
|
||||
z-index: -1;
|
||||
}
|
||||
h1 {
|
||||
color: #1976d2;
|
||||
margin-bottom: 20px;
|
||||
font-size: 2.5em;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.icon {
|
||||
font-size: 3em;
|
||||
color: #ff9800;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
p {
|
||||
color: #424242;
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.highlight {
|
||||
color: #2196f3;
|
||||
font-weight: 600;
|
||||
}
|
||||
.contact-info {
|
||||
background-color: #f9f9f9;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
margin-top: 20px;
|
||||
border-left: 4px solid #1976d2;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="icon">📞</div>
|
||||
<h1>We're Sorry, {{ doc.party_name or doc.customer }}</h1>
|
||||
<p>We understand that our quote didn't meet your needs this time. We'd still love to discuss how we can help with your project!</p>
|
||||
<p>Please don't hesitate to reach out:</p>
|
||||
<div class="contact-info">
|
||||
<p><strong>Phone:</strong> [Your Company Phone Number]</p>
|
||||
<p><strong>Email:</strong> [Your Company Email]</p>
|
||||
<p><strong>Website:</strong> [Your Company Website]</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
86
custom_ui/templates/estimates/request-call.html
Normal file
86
custom_ui/templates/estimates/request-call.html
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Call Requested</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background: linear-gradient(135deg, #e8f5e8 0%, #c8e6c9 100%);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
background-color: white;
|
||||
padding: 50px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4);
|
||||
opacity: 0.1;
|
||||
transform: rotate(45deg);
|
||||
z-index: -1;
|
||||
}
|
||||
h1 {
|
||||
color: #1976d2;
|
||||
margin-bottom: 20px;
|
||||
font-size: 2.5em;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.icon {
|
||||
font-size: 3em;
|
||||
color: #ff9800;
|
||||
margin-bottom: 20px;
|
||||
animation: bounce 1s ease-in-out;
|
||||
}
|
||||
p {
|
||||
color: #424242;
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.highlight {
|
||||
color: #2196f3;
|
||||
font-weight: 600;
|
||||
}
|
||||
@keyframes bounce {
|
||||
0%, 20%, 50%, 80%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
60% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="icon">📞</div>
|
||||
<h1>Thank You, {{ doc.party_name or doc.customer }}!</h1>
|
||||
<p>Thank you for your response! Someone from our team will <span class="highlight">reach out to you soon</span> to discuss your project.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue