job creation working
This commit is contained in:
parent
bd9e00c6f1
commit
d3818d1985
@ -310,7 +310,6 @@ def upsert_client(data):
|
||||
"phone": primary_contact.get("phone_number"),
|
||||
"custom_customer_name": customer_name,
|
||||
"customer_type": customer_type,
|
||||
"address_type": "Billing",
|
||||
"companies": [{ "company": data.get("company_name")
|
||||
}]
|
||||
}
|
||||
@ -338,7 +337,7 @@ def upsert_client(data):
|
||||
"last_name": contact_data.get("last_name"),
|
||||
"role": contact_data.get("contact_role", "Other"),
|
||||
"custom_email": contact_data.get("email"),
|
||||
"is_primary_contact":1 if contact_data.get("is_primary", False) else 0,
|
||||
"is_primary_contact": 1 if contact_data.get("is_primary", False) else 0,
|
||||
"customer_type": "Lead",
|
||||
"customer_name": client_doc.name,
|
||||
"email_ids": [{
|
||||
@ -370,11 +369,13 @@ def upsert_client(data):
|
||||
# Handle address creation
|
||||
address_docs = []
|
||||
for address in addresses:
|
||||
is_billing = True if address.get("is_billing_address") else False
|
||||
print("#####DEBUG: Creating address with data:", address)
|
||||
address_doc = AddressService.create_address({
|
||||
"address_title": build_address_title(customer_name, address),
|
||||
"address_title": AddressService.build_address_title(customer_name, address),
|
||||
"address_line1": address.get("address_line1"),
|
||||
"address_line2": address.get("address_line2"),
|
||||
"address_type": "Billing" if is_billing else "Service",
|
||||
"city": address.get("city"),
|
||||
"state": address.get("state"),
|
||||
"country": "United States",
|
||||
@ -385,6 +386,9 @@ def upsert_client(data):
|
||||
})
|
||||
AddressService.link_address_to_customer(address_doc, "Lead", client_doc.name)
|
||||
address_doc.reload()
|
||||
if is_billing:
|
||||
client_doc.custom_billing_address = address_doc.name
|
||||
client_doc.save(ignore_permissions=True)
|
||||
for contact_to_link_idx in address.get("contacts", []):
|
||||
contact_doc = contact_docs[contact_to_link_idx]
|
||||
AddressService.link_address_to_contact(address_doc, contact_doc.name)
|
||||
|
||||
@ -37,7 +37,7 @@ def get_estimate_table_data(filters={}, sortings=[], page=1, page_size=10):
|
||||
|
||||
tableRows = []
|
||||
for estimate in estimates:
|
||||
full_address = frappe.db.get_value("Address", estimate.get("custom_installation_address"), "full_address")
|
||||
full_address = frappe.db.get_value("Address", estimate.get("custom_job_address"), "full_address")
|
||||
tableRow = {}
|
||||
tableRow["id"] = estimate["name"]
|
||||
tableRow["address"] = full_address
|
||||
@ -69,7 +69,7 @@ def get_estimate(estimate_name):
|
||||
estimate = frappe.get_doc("Quotation", estimate_name)
|
||||
est_dict = estimate.as_dict()
|
||||
|
||||
address_name = estimate.custom_installation_address or estimate.customer_address
|
||||
address_name = estimate.custom_job_address or estimate.customer_address
|
||||
if address_name:
|
||||
# Fetch Address Doc
|
||||
address_doc = frappe.get_doc("Address", address_name).as_dict()
|
||||
@ -386,8 +386,6 @@ def upsert_estimate(data):
|
||||
print("DEBUG: Upsert estimate data:", data)
|
||||
address_doc = AddressService.get_or_throw(data.get("address_name"))
|
||||
estimate_name = data.get("estimate_name")
|
||||
client_doctype = ClientService.get_client_doctype(address_doc.customer_name)
|
||||
print("DEBUG: Retrieved client doctype:", client_doctype)
|
||||
project_template = data.get("project_template", None)
|
||||
|
||||
# If estimate_name exists, update existing estimate
|
||||
@ -432,6 +430,12 @@ def upsert_estimate(data):
|
||||
else:
|
||||
print("DEBUG: Creating new estimate")
|
||||
print("DEBUG: Retrieved address name:", data.get("address_name"))
|
||||
client_doc = ClientService.get_client_or_throw(address_doc.customer_name)
|
||||
# billing_address = next((addr for addr in address_doc if addr.address_type == "Billing"), None)
|
||||
# if billing_address:
|
||||
# print("DEBUG: Found billing address:", billing_address.name)
|
||||
# else:
|
||||
# print("DEBUG: No billing address found for client:", client_doc.name)
|
||||
new_estimate = frappe.get_doc({
|
||||
"doctype": "Quotation",
|
||||
"custom_requires_half_payment": data.get("requires_half_payment", 0),
|
||||
@ -441,9 +445,9 @@ def upsert_estimate(data):
|
||||
"party_name": data.get("contact_name"),
|
||||
"quotation_to": "Contact",
|
||||
"company": data.get("company"),
|
||||
"actual_customer_name": address_doc.customer_name,
|
||||
"customer_type": client_doctype,
|
||||
"customer_address": data.get("address_name"),
|
||||
"actual_customer_name": client_doc.name,
|
||||
"customer_type": address_doc.customer_type,
|
||||
"customer_address": client_doc.custom_billing_address,
|
||||
"contact_person": data.get("contact_name"),
|
||||
"letter_head": data.get("company"),
|
||||
"custom_project_template": data.get("project_template", None),
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import frappe, json
|
||||
from custom_ui.db_utils import process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response
|
||||
from custom_ui.services import AddressService, ClientService
|
||||
|
||||
# ===============================================================================
|
||||
# JOB MANAGEMENT API METHODS
|
||||
@ -45,6 +46,10 @@ def get_job(job_id=""):
|
||||
print("DEBUG: Loading Job from database:", job_id)
|
||||
try:
|
||||
project = frappe.get_doc("Project", job_id)
|
||||
address_doc = AddressService.get_or_throw(project.job_address)
|
||||
project = project.as_dict()
|
||||
project["job_address"] = address_doc
|
||||
project["client"] = ClientService.get_client_or_throw(project.customer)
|
||||
return build_success_response(project)
|
||||
except Exception as e:
|
||||
return build_error_response(str(e), 500)
|
||||
|
||||
@ -1,6 +1,18 @@
|
||||
import frappe
|
||||
from custom_ui.db_utils import process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response
|
||||
from custom_ui.services import DbService
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_task_status(task_name, new_status):
|
||||
"""Set the status of a specific task."""
|
||||
try:
|
||||
task = DbService.get_or_throw("Task", task_name)
|
||||
task.status = new_status
|
||||
task.save()
|
||||
return build_success_response(f"Task {task_name} status updated to {new_status}.")
|
||||
except Exception as e:
|
||||
return build_error_response(str(e), 500)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_job_task_list(job_id=""):
|
||||
|
||||
@ -7,6 +7,9 @@ def after_insert(doc, method):
|
||||
AddressService.append_link_v2(
|
||||
doc.custom_job_address, "quotations", {"quotation": doc.name, "project_template": doc.custom_project_template}
|
||||
)
|
||||
AddressService.append_link_v2(
|
||||
doc.custom_job_address, "links", {"link_doctype": "Quotation", "link_name": doc.name}
|
||||
)
|
||||
ClientService.append_link_v2(
|
||||
doc.actual_customer_name, "quotations", {"quotation": doc.name, "project_template": doc.custom_project_template}
|
||||
)
|
||||
@ -74,11 +77,11 @@ def on_update_after_submit(doc, method):
|
||||
new_sales_order.set_payment_schedule()
|
||||
print("DEBUG: Inserting Sales Order:", new_sales_order.as_dict())
|
||||
new_sales_order.delivery_date = new_sales_order.transaction_date
|
||||
backup = new_sales_order.customer_address
|
||||
new_sales_order.customer_address = None
|
||||
# backup = new_sales_order.customer_address
|
||||
# new_sales_order.customer_address = None
|
||||
new_sales_order.insert()
|
||||
print("DEBUG: Submitting Sales Order")
|
||||
new_sales_order.customer_address = backup
|
||||
# new_sales_order.customer_address = backup
|
||||
new_sales_order.submit()
|
||||
frappe.db.commit()
|
||||
print("DEBUG: Sales Order created successfully:", new_sales_order.name)
|
||||
|
||||
@ -1,5 +1,28 @@
|
||||
import frappe
|
||||
from custom_ui.services import AddressService, ClientService
|
||||
|
||||
|
||||
def after_insert(doc, method):
|
||||
print("DEBUG: After Insert Triggered for Project")
|
||||
print("DEBUG: Linking Project to address and Customer")
|
||||
AddressService.append_link_v2(
|
||||
doc.job_address, "projects", {"project": doc.name, "project_template": doc.project_template}
|
||||
)
|
||||
AddressService.append_link_v2(
|
||||
doc.job_address, "links", {"link_doctype": "Project", "link_name": doc.name}
|
||||
)
|
||||
ClientService.append_link_v2(
|
||||
doc.customer, "projects", {"project": doc.name, "project_template": doc.project_template}
|
||||
)
|
||||
if doc.project_template == "SNW Install":
|
||||
print("DEBUG: Project template is SNW Install, updating Address status to In Progress")
|
||||
AddressService.update_value(
|
||||
doc.job_address,
|
||||
"job_status",
|
||||
"In Progress"
|
||||
)
|
||||
|
||||
|
||||
def before_insert(doc, method):
|
||||
# This is where we will add logic to set tasks and other properties of a job based on it's project_template
|
||||
pass
|
||||
@ -14,6 +14,7 @@ def after_insert(doc, method):
|
||||
print("DEBUG: After Insert Triggered for On-Site Meeting")
|
||||
print("DEBUG: Linking bid meeting to customer and address")
|
||||
AddressService.append_link_v2(doc.address, "onsite_meetings", {"onsite_meeting": doc.name, "project_template": doc.project_template})
|
||||
AddressService.append_link_v2(doc.address, "links", {"link_doctype": "On-Site Meeting", "link_name": doc.name})
|
||||
ClientService.append_link(doc.party_name, "onsite_meetings", "onsite_meeting", doc.name)
|
||||
if doc.project_template == "SNW Install":
|
||||
print("DEBUG: Project template is SNW Install, updating Address status to In Progress")
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
import frappe
|
||||
from custom_ui.services import DbService
|
||||
from custom_ui.services import DbService, AddressService, ClientService
|
||||
|
||||
def before_insert(doc, method):
|
||||
print("DEBUG: before_insert hook triggered for Sales Order:", doc.name)
|
||||
if doc.custom_project_template == "SNW Install":
|
||||
print("DEBUG: Sales Order uses SNW Install template, checking for duplicate linked sales orders.")
|
||||
address_doc = AddressService.get_or_throw(doc.custom_job_address)
|
||||
if "SNW Install" in [link.project_template for link in address_doc.sales_orders]:
|
||||
raise frappe.ValidationError("A Sales Order with project template 'SNW Install' is already linked to this address.")
|
||||
|
||||
def on_submit(doc, method):
|
||||
print("DEBUG: Info from Sales Order")
|
||||
@ -7,27 +15,39 @@ def on_submit(doc, method):
|
||||
print(doc.company)
|
||||
print(doc.transaction_date)
|
||||
print(doc.customer)
|
||||
print(doc.custom_job_address)
|
||||
print(doc.custom_project_template)
|
||||
# Create Invoice and Project from Sales Order
|
||||
try:
|
||||
print("Creating Project from Sales Order", doc.name)
|
||||
sales_order = frappe.get_doc("Sales Order", doc.name)
|
||||
if not sales_order.custom_project_template:
|
||||
return
|
||||
project_template = DbService.get("Project Template", sales_order.custom_project_template)
|
||||
new_job = frappe.get_doc({
|
||||
"doctype": "Project",
|
||||
"custom_job_address": sales_order.custom_job_address,
|
||||
"project_name": f"{sales_order.custom_project_template} - {sales_order.custom_job_address}",
|
||||
"project_template": project_template.name,
|
||||
"custom_warranty_duration_days": 90,
|
||||
"sales_order": sales_order.name
|
||||
})
|
||||
# attatch the job to the sales_order links
|
||||
new_job.insert()
|
||||
frappe.db.commit()
|
||||
if doc.custom_project_template or doc.project_template:
|
||||
new_job = frappe.get_doc({
|
||||
"doctype": "Project",
|
||||
"custom_job_address": doc.custom_job_address,
|
||||
"project_name": f"{doc.custom_project_template} - {doc.custom_job_address}",
|
||||
"project_template": doc.custom_project_template,
|
||||
"custom_warranty_duration_days": 90,
|
||||
"customer": doc.customer,
|
||||
"job_address": doc.custom_job_address,
|
||||
"sales_order": doc.name
|
||||
})
|
||||
# attatch the job to the sales_order links
|
||||
new_job.insert()
|
||||
frappe.db.commit()
|
||||
except Exception as e:
|
||||
print("ERROR creating Project from Sales Order:", str(e))
|
||||
|
||||
def after_insert(doc, method):
|
||||
print("DEBUG: after_insert hook triggered for Sales Order:", doc.name)
|
||||
AddressService.append_link_v2(
|
||||
doc.custom_job_address, "sales_orders", {"sales_order": doc.name, "project_template": doc.custom_project_template}
|
||||
)
|
||||
AddressService.append_link_v2(
|
||||
doc.custom_job_address, "links", {"link_doctype": "Sales Order", "link_name": doc.name}
|
||||
)
|
||||
ClientService.append_link_v2(
|
||||
doc.customer, "sales_orders", {"sales_order": doc.name, "project_template": doc.custom_project_template}
|
||||
)
|
||||
|
||||
def create_sales_invoice_from_sales_order(doc, method):
|
||||
try:
|
||||
|
||||
@ -114,8 +114,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.065211",
|
||||
"migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2",
|
||||
"modified": "2026-01-16 03:16:17.476708",
|
||||
"module": "Custom",
|
||||
"name": "Lead Companies Link",
|
||||
"naming_rule": "",
|
||||
@ -332,8 +332,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.117683",
|
||||
"migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2",
|
||||
"modified": "2026-01-16 03:16:17.527399",
|
||||
"module": "Custom",
|
||||
"name": "Address Project Link",
|
||||
"naming_rule": "",
|
||||
@ -550,8 +550,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.173958",
|
||||
"migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2",
|
||||
"modified": "2026-01-16 03:16:17.579759",
|
||||
"module": "Custom",
|
||||
"name": "Address Quotation Link",
|
||||
"naming_rule": "",
|
||||
@ -768,8 +768,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.229451",
|
||||
"migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2",
|
||||
"modified": "2026-01-16 03:16:17.630680",
|
||||
"module": "Custom",
|
||||
"name": "Address On-Site Meeting Link",
|
||||
"naming_rule": "",
|
||||
@ -901,6 +901,70 @@
|
||||
"trigger": null,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"collapsible_depends_on": null,
|
||||
"columns": 0,
|
||||
"default": null,
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"documentation_url": null,
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "project_template",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"is_virtual": 0,
|
||||
"label": "Project Template",
|
||||
"length": 0,
|
||||
"link_filters": null,
|
||||
"make_attachment_public": 0,
|
||||
"mandatory_depends_on": null,
|
||||
"max_height": null,
|
||||
"no_copy": 0,
|
||||
"non_negative": 0,
|
||||
"oldfieldname": null,
|
||||
"oldfieldtype": null,
|
||||
"options": "Project Template",
|
||||
"parent": "Address Sales Order Link",
|
||||
"parentfield": "fields",
|
||||
"parenttype": "DocType",
|
||||
"permlevel": 0,
|
||||
"placeholder": null,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 0,
|
||||
"read_only_depends_on": null,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"show_dashboard": 0,
|
||||
"show_on_timeline": 0,
|
||||
"show_preview_popup": 0,
|
||||
"sort_options": 0,
|
||||
"translatable": 0,
|
||||
"trigger": null,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
}
|
||||
],
|
||||
"force_re_route_to_default_view": 0,
|
||||
@ -922,8 +986,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.280251",
|
||||
"migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2",
|
||||
"modified": "2026-01-16 03:19:33.624850",
|
||||
"module": "Custom",
|
||||
"name": "Address Sales Order Link",
|
||||
"naming_rule": "",
|
||||
@ -1076,8 +1140,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.332720",
|
||||
"migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2",
|
||||
"modified": "2026-01-16 03:16:17.729324",
|
||||
"module": "Custom",
|
||||
"name": "Contact Address Link",
|
||||
"naming_rule": "",
|
||||
@ -1230,8 +1294,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.385343",
|
||||
"migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2",
|
||||
"modified": "2026-01-16 03:16:17.780526",
|
||||
"module": "Custom",
|
||||
"name": "Lead On-Site Meeting Link",
|
||||
"naming_rule": "",
|
||||
@ -1832,8 +1896,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.457103",
|
||||
"migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2",
|
||||
"modified": "2026-01-16 03:16:17.847539",
|
||||
"module": "Selling",
|
||||
"name": "Quotation Template",
|
||||
"naming_rule": "",
|
||||
@ -2330,8 +2394,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.529451",
|
||||
"migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2",
|
||||
"modified": "2026-01-16 03:16:17.916692",
|
||||
"module": "Selling",
|
||||
"name": "Quotation Template Item",
|
||||
"naming_rule": "",
|
||||
@ -2484,8 +2548,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.580626",
|
||||
"migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2",
|
||||
"modified": "2026-01-16 03:16:17.965428",
|
||||
"module": "Custom UI",
|
||||
"name": "Customer Company Link",
|
||||
"naming_rule": "",
|
||||
@ -2638,8 +2702,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.630918",
|
||||
"migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2",
|
||||
"modified": "2026-01-16 03:16:18.018887",
|
||||
"module": "Custom UI",
|
||||
"name": "Customer Address Link",
|
||||
"naming_rule": "",
|
||||
@ -2792,8 +2856,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.681698",
|
||||
"migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2",
|
||||
"modified": "2026-01-16 03:16:18.066488",
|
||||
"module": "Custom UI",
|
||||
"name": "Customer Contact Link",
|
||||
"naming_rule": "",
|
||||
@ -2946,8 +3010,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.731378",
|
||||
"migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2",
|
||||
"modified": "2026-01-16 03:16:18.115936",
|
||||
"module": "Custom",
|
||||
"name": "Address Contact Link",
|
||||
"naming_rule": "",
|
||||
@ -3100,8 +3164,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.783154",
|
||||
"migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2",
|
||||
"modified": "2026-01-16 03:16:18.164485",
|
||||
"module": "Custom",
|
||||
"name": "Customer On-Site Meeting Link",
|
||||
"naming_rule": "",
|
||||
@ -3254,8 +3318,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.832605",
|
||||
"migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2",
|
||||
"modified": "2026-01-16 03:16:18.212622",
|
||||
"module": "Custom",
|
||||
"name": "Customer Project Link",
|
||||
"naming_rule": "",
|
||||
@ -3408,8 +3472,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.882511",
|
||||
"migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2",
|
||||
"modified": "2026-01-16 03:16:18.262231",
|
||||
"module": "Custom",
|
||||
"name": "Customer Quotation Link",
|
||||
"naming_rule": "",
|
||||
@ -3562,8 +3626,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.934072",
|
||||
"migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2",
|
||||
"modified": "2026-01-16 03:16:18.311056",
|
||||
"module": "Custom",
|
||||
"name": "Customer Sales Order Link",
|
||||
"naming_rule": "",
|
||||
@ -3716,8 +3780,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:57.984126",
|
||||
"migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2",
|
||||
"modified": "2026-01-16 03:16:18.359782",
|
||||
"module": "Custom",
|
||||
"name": "Lead Address Link",
|
||||
"naming_rule": "",
|
||||
@ -3870,8 +3934,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:58.034758",
|
||||
"migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2",
|
||||
"modified": "2026-01-16 03:16:18.408094",
|
||||
"module": "Custom",
|
||||
"name": "Lead Contact Link",
|
||||
"naming_rule": "",
|
||||
@ -4024,8 +4088,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:58.086245",
|
||||
"migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2",
|
||||
"modified": "2026-01-16 03:16:18.457175",
|
||||
"module": "Custom",
|
||||
"name": "Lead Quotation Link",
|
||||
"naming_rule": "",
|
||||
@ -4178,8 +4242,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "717d2b4e3d605ae371d8c05bb650b364",
|
||||
"modified": "2026-01-15 10:05:58.139052",
|
||||
"migration_hash": "633d6c4c53baf7641fc8ffe06fc9e3f2",
|
||||
"modified": "2026-01-16 03:16:18.506817",
|
||||
"module": "Custom",
|
||||
"name": "Address Company Link",
|
||||
"naming_rule": "",
|
||||
|
||||
@ -175,8 +175,14 @@ doc_events = {
|
||||
"on_update_after_submit": "custom_ui.events.estimate.on_update_after_submit"
|
||||
},
|
||||
"Sales Order": {
|
||||
"before_insert": "custom_ui.events.sales_order.before_insert",
|
||||
"after_insert": "custom_ui.events.sales_order.after_insert",
|
||||
"on_submit": "custom_ui.events.sales_order.on_submit"
|
||||
},
|
||||
"Project": {
|
||||
"before_insert": "custom_ui.events.jobs.before_insert",
|
||||
"after_insert": "custom_ui.events.jobs.after_insert"
|
||||
},
|
||||
"Task": {
|
||||
"before_insert": "custom_ui.events.task.before_insert"
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ def after_migrate():
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
frappe.reload_doctype(doctype)
|
||||
|
||||
update_address_fields()
|
||||
# update_address_fields()
|
||||
# build_frontend()
|
||||
|
||||
|
||||
@ -70,6 +70,17 @@ def add_custom_fields():
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
|
||||
print("\n🔧 Adding custom fields to doctypes...")
|
||||
|
||||
try:
|
||||
address_meta = frappe.get_meta("Address")
|
||||
address_type_params = address_meta.get_field("address_type")
|
||||
if address_type_params and "Service" not in (address_type_params.options or ""):
|
||||
print(" Adding 'Service' to Address type options...")
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
make_property_setter("Address", "address_type", "options", (address_type_params.options or "") + "\nService", "DocField")
|
||||
print(" ✅ Added 'Service' to Address address_type options.")
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Failed to update Address address_type: {e}")
|
||||
|
||||
custom_fields = {
|
||||
"Customer": [
|
||||
@ -145,6 +156,13 @@ def add_custom_fields():
|
||||
options="Lead On-Site Meeting Link",
|
||||
insert_after="quotations"
|
||||
),
|
||||
dict(
|
||||
fieldname="custom_billing_address",
|
||||
label="Custom Address",
|
||||
fieldtype="Link",
|
||||
options="Address",
|
||||
insert_after="customer_name"
|
||||
),
|
||||
dict(
|
||||
fieldname="quotations",
|
||||
label="Quotations",
|
||||
@ -505,6 +523,24 @@ def add_custom_fields():
|
||||
allow_on_submit=1
|
||||
)
|
||||
],
|
||||
"Project": [
|
||||
dict(
|
||||
fieldname="job_address",
|
||||
label="Job Address",
|
||||
fieldtype="Link",
|
||||
options="Address",
|
||||
insert_after="project_name",
|
||||
description="The address where the job is being performed."
|
||||
),
|
||||
dict(
|
||||
fieldname="customer",
|
||||
label="Customer",
|
||||
fieldtype="Link",
|
||||
options="Customer",
|
||||
insert_after="job_address",
|
||||
description="The customer for whom the project is being executed."
|
||||
)
|
||||
],
|
||||
"Project Template": [
|
||||
dict(
|
||||
fieldname="company",
|
||||
|
||||
@ -1,13 +1,22 @@
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
import requests
|
||||
from .contact_service import ContactService, DbService
|
||||
|
||||
class AddressService:
|
||||
|
||||
@staticmethod
|
||||
def build_address_title(customer_name, address_data) -> str:
|
||||
"""Build a title for the address based on its fields."""
|
||||
print(f"DEBUG: Building address title for customer '{customer_name}' with address data: {address_data}")
|
||||
is_billing = address_data.get("is_billing_address", False)
|
||||
address_type = "Billing" if is_billing else "Service"
|
||||
return f"{customer_name} - {address_data.get('address_line1', '')} {address_data.get('city')} - {address_type}"
|
||||
|
||||
@staticmethod
|
||||
def build_full_dict(
|
||||
address_doc,
|
||||
included_links: list = ["contacts", "on-site meetings", "quotations", "sales orders", "projects", "companies"]) -> dict:
|
||||
address_doc: Document,
|
||||
included_links: list = ["contacts", "on-site meetings", "quotations", "sales orders", "projects", "companies"]) -> frappe._dict:
|
||||
"""Build a full dictionary representation of an address, including all links. Can optionally exclude links."""
|
||||
print(f"DEBUG: Building full dict for Address {address_doc.name}")
|
||||
address_dict = address_doc.as_dict()
|
||||
@ -27,12 +36,12 @@ class AddressService:
|
||||
return address_dict
|
||||
|
||||
@staticmethod
|
||||
def get_address_by_full_address(full_address: str):
|
||||
def get_address_by_full_address(full_address: str) -> Document:
|
||||
"""Retrieve an address document by its full_address field. Returns None if not found."""
|
||||
print(f"DEBUG: Retrieving Address document with full_address: {full_address}")
|
||||
address_name = frappe.db.get_value("Address", {"full_address": full_address})
|
||||
if address_name:
|
||||
address_doc = frappe.get_doc("Address", address_name)
|
||||
address_doc = DbService.get_or_throw("Address", address_name)
|
||||
print("DEBUG: Address document found.")
|
||||
return address_doc
|
||||
print("DEBUG: Address document not found.")
|
||||
@ -47,18 +56,18 @@ class AddressService:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get(address_name: str):
|
||||
def get(address_name: str) -> Document:
|
||||
"""Retrieve an address document by name. Returns None if not found."""
|
||||
print(f"DEBUG: Retrieving Address document with name: {address_name}")
|
||||
if AddressService.exists(address_name):
|
||||
address_doc = frappe.get_doc("Address", address_name)
|
||||
address_doc = DbService.get_or_throw("Address", address_name)
|
||||
print("DEBUG: Address document found.")
|
||||
return address_doc
|
||||
print("DEBUG: Address document not found.")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_or_throw(address_name: str):
|
||||
def get_or_throw(address_name: str) -> Document:
|
||||
"""Retrieve an address document by name or throw an error if not found."""
|
||||
address_doc = AddressService.get(address_name)
|
||||
if address_doc:
|
||||
@ -66,43 +75,43 @@ class AddressService:
|
||||
raise ValueError(f"Address with name {address_name} does not exist.")
|
||||
|
||||
@staticmethod
|
||||
def update_value(docname: str, fieldname: str, value, save: bool = True) -> frappe._dict:
|
||||
def update_value(doc_name: str, fieldname: str, value, save: bool = True) -> Document:
|
||||
"""Update a specific field value of a document."""
|
||||
print(f"DEBUG: Updating Address {docname}, setting {fieldname} to {value}")
|
||||
if AddressService.exists(docname) is False:
|
||||
raise ValueError(f"Address with name {docname} does not exist.")
|
||||
print(f"DEBUG: Updating Address {doc_name}, setting {fieldname} to {value}")
|
||||
if AddressService.exists(doc_name) is False:
|
||||
raise ValueError(f"Address with name {doc_name} does not exist.")
|
||||
if save:
|
||||
print("DEBUG: Saving updated Address document.")
|
||||
address_doc = AddressService.get_or_throw(docname)
|
||||
address_doc = AddressService.get_or_throw(doc_name)
|
||||
setattr(address_doc, fieldname, value)
|
||||
address_doc.save(ignore_permissions=True)
|
||||
else:
|
||||
print("DEBUG: Not saving Address document as save=False.")
|
||||
frappe.db.set_value("Address", docname, fieldname, value)
|
||||
print(f"DEBUG: Updated Address {docname}: set {fieldname} to {value}")
|
||||
frappe.db.set_value("Address", doc_name, fieldname, value)
|
||||
print(f"DEBUG: Updated Address {doc_name}: set {fieldname} to {value}")
|
||||
return address_doc
|
||||
|
||||
@staticmethod
|
||||
def get_value(docname: str, fieldname: str):
|
||||
def get_value(doc_name: str, fieldname: str) -> any:
|
||||
"""Get a specific field value of a document. Returns None if document does not exist."""
|
||||
print(f"DEBUG: Getting value of field {fieldname} from Address {docname}")
|
||||
if not AddressService.exists(docname):
|
||||
print(f"DEBUG: Getting value of field {fieldname} from Address {doc_name}")
|
||||
if not AddressService.exists(doc_name):
|
||||
print("DEBUG: Value cannot be retrieved; Address does not exist.")
|
||||
return None
|
||||
value = frappe.db.get_value("Address", docname, fieldname)
|
||||
value = frappe.db.get_value("Address", doc_name, fieldname)
|
||||
print(f"DEBUG: Retrieved value: {value}")
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def get_value_or_throw(docname: str, fieldname: str):
|
||||
def get_value_or_throw(doc_name: str, fieldname: str) -> any:
|
||||
"""Get a specific field value of a document or throw an error if document does not exist."""
|
||||
value = AddressService.get_value(docname, fieldname)
|
||||
value = AddressService.get_value(doc_name, fieldname)
|
||||
if value is not None:
|
||||
return value
|
||||
raise ValueError(f"Address with name {docname} does not exist.")
|
||||
raise ValueError(f"Address with name {doc_name} does not exist.")
|
||||
|
||||
@staticmethod
|
||||
def create(address_data: dict):
|
||||
def create(address_data: dict) -> Document:
|
||||
"""Create a new address."""
|
||||
print("DEBUG: Creating new Address with data:", address_data)
|
||||
address = frappe.get_doc({
|
||||
@ -114,26 +123,34 @@ class AddressService:
|
||||
return address
|
||||
|
||||
@staticmethod
|
||||
def link_address_to_customer(address_doc, customer_type, customer_name):
|
||||
def link_address_to_customer(address_doc: Document, customer_type: str, customer_name: str):
|
||||
"""Link an address to a customer or lead."""
|
||||
print(f"DEBUG: Linking Address {address_doc.name} to {customer_type} {customer_name}")
|
||||
address_doc.customer_type = customer_type
|
||||
address_doc.customer_name = customer_name
|
||||
address_doc.append("links", {
|
||||
"link_doctype": customer_type,
|
||||
"link_name": customer_name
|
||||
})
|
||||
address_doc.save(ignore_permissions=True)
|
||||
print(f"DEBUG: Linked Address {address_doc.name} to {customer_type} {customer_name}")
|
||||
|
||||
@staticmethod
|
||||
def link_address_to_contact(address_doc, contact_name):
|
||||
def link_address_to_contact(address_doc: Document, contact_name: str):
|
||||
"""Link an address to a contact."""
|
||||
print(f"DEBUG: Linking Address {address_doc.name} to Contact {contact_name}")
|
||||
address_doc.append("contacts", {
|
||||
"contact": contact_name
|
||||
})
|
||||
address_doc.append("links", {
|
||||
"link_doctype": "Contact",
|
||||
"link_name": contact_name
|
||||
})
|
||||
address_doc.save(ignore_permissions=True)
|
||||
print(f"DEBUG: Linked Address {address_doc.name} to Contact {contact_name}")
|
||||
|
||||
@staticmethod
|
||||
def create_address(address_data):
|
||||
def create_address(address_data: dict) -> Document:
|
||||
"""Create a new address."""
|
||||
address = frappe.get_doc({
|
||||
"doctype": "Address",
|
||||
@ -172,7 +189,7 @@ class AddressService:
|
||||
print(f"DEBUG: Set link field {field} for Address {address_name} with link data {link}")
|
||||
|
||||
@staticmethod
|
||||
def get_county_and_set(address_doc, save: bool = False):
|
||||
def get_county_and_set(address_doc: Document, save: bool = False):
|
||||
"""Get the county from the address document and set it if not already set."""
|
||||
if not address_doc.county:
|
||||
print(f"DEBUG: Getting county for Address {address_doc.name}")
|
||||
@ -196,7 +213,11 @@ class AddressService:
|
||||
except (KeyError, IndexError):
|
||||
return None
|
||||
|
||||
return {
|
||||
county_info = {
|
||||
"county": county,
|
||||
"county_fips": county_fips
|
||||
}
|
||||
}
|
||||
|
||||
AddressService.update_value(address_doc.name, "county", county_info, save)
|
||||
AddressService.update_value(address_doc.name, "county_fips", county_fips, save)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from .db_service import DbService
|
||||
from erpnext.crm.doctype.lead.lead import make_customer
|
||||
from .address_service import AddressService
|
||||
@ -8,6 +9,12 @@ from .onsite_meeting_service import OnSiteMeetingService
|
||||
|
||||
class ClientService:
|
||||
|
||||
@staticmethod
|
||||
def get_client_or_throw(client_name: str) -> Document:
|
||||
"""Retrieve a Client document (Customer or Lead) or throw an error if it does not exist."""
|
||||
doctype = ClientService.get_client_doctype(client_name)
|
||||
return DbService.get_or_throw(doctype, client_name)
|
||||
|
||||
@staticmethod
|
||||
def get_client_doctype(client_name: str) -> str:
|
||||
"""Determine if the client is a Customer or Lead."""
|
||||
@ -56,8 +63,9 @@ class ClientService:
|
||||
update_quotations: bool = True,
|
||||
update_addresses: bool = True,
|
||||
update_contacts: bool = True,
|
||||
update_onsite_meetings: bool = True
|
||||
):
|
||||
update_onsite_meetings: bool = True,
|
||||
update_companies: bool = True
|
||||
) -> Document:
|
||||
"""Convert a Lead to a Customer."""
|
||||
print(f"DEBUG: Converting Lead {lead_name} to Customer")
|
||||
try:
|
||||
@ -68,7 +76,7 @@ class ClientService:
|
||||
customer_doc = make_customer(lead_doc.name)
|
||||
print(f"DEBUG: make_customer() returned document type: {type(customer_doc)}")
|
||||
print(f"DEBUG: Customer doc name: {customer_doc.name if hasattr(customer_doc, 'name') else 'NO NAME'}")
|
||||
|
||||
customer_doc.custom_billing_address = lead_doc.custom_billing_address
|
||||
print("DEBUG: Calling customer_doc.insert()")
|
||||
customer_doc.insert(ignore_permissions=True)
|
||||
print(f"DEBUG: Customer inserted successfully: {customer_doc.name}")
|
||||
@ -128,6 +136,16 @@ class ClientService:
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to link onsite meeting {meeting.get('onsite_meeting')}: {str(e)}")
|
||||
frappe.log_error(f"Onsite meeting linking error: {str(e)}", "convert_lead_to_customer")
|
||||
if update_companies:
|
||||
print(f"DEBUG: Updating companies. Count: {len(lead_doc.get('companies', []))}")
|
||||
for company in lead_doc.get("companies", []):
|
||||
try:
|
||||
print(f"DEBUG: Processing company: {company.get('company')}")
|
||||
ClientService.append_link_v2(customer_doc.name, "companies", {"company": company.get("company")})
|
||||
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}")
|
||||
return customer_doc
|
||||
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from .db_service import DbService
|
||||
|
||||
class ContactService:
|
||||
|
||||
@staticmethod
|
||||
def create(data: dict):
|
||||
def create(data: dict) -> Document:
|
||||
"""Create a new contact."""
|
||||
print("DEBUG: Creating new Contact with data:", data)
|
||||
contact = frappe.get_doc({
|
||||
@ -16,25 +17,33 @@ class ContactService:
|
||||
return contact
|
||||
|
||||
@staticmethod
|
||||
def link_contact_to_customer(contact_doc, customer_type, customer_name):
|
||||
def link_contact_to_customer(contact_doc: Document, customer_type: str, customer_name: str):
|
||||
"""Link a contact to a customer or lead."""
|
||||
print(f"DEBUG: Linking Contact {contact_doc.name} to {customer_type} {customer_name}")
|
||||
contact_doc.customer_type = customer_type
|
||||
contact_doc.customer_name = customer_name
|
||||
contact_doc.append("links", {
|
||||
"link_doctype": customer_type,
|
||||
"link_name": customer_name
|
||||
})
|
||||
contact_doc.save(ignore_permissions=True)
|
||||
print(f"DEBUG: Linked Contact {contact_doc.name} to {customer_type} {customer_name}")
|
||||
|
||||
@staticmethod
|
||||
def link_contact_to_address(contact_doc, address_name):
|
||||
def link_contact_to_address(contact_doc: Document, address_name: str):
|
||||
"""Link an address to a contact."""
|
||||
print(f"DEBUG: Linking Address {address_name} to Contact {contact_doc.name}")
|
||||
contact_doc.append("addresses", {
|
||||
"address": address_name
|
||||
})
|
||||
contact_doc.append("links", {
|
||||
"link_doctype": "Address",
|
||||
"link_name": address_name
|
||||
})
|
||||
contact_doc.save(ignore_permissions=True)
|
||||
print(f"DEBUG: Linked Address {address_name} to Contact {contact_doc.name}")
|
||||
|
||||
@staticmethod
|
||||
def get_or_throw(contact_name: str):
|
||||
def get_or_throw(contact_name: str) -> Document:
|
||||
"""Retrieve a Contact document or throw an error if it does not exist."""
|
||||
return DbService.get_or_throw("Contact", contact_name)
|
||||
@ -394,7 +394,11 @@ class Api {
|
||||
static async getTaskStatusOptions() {
|
||||
console.log("DEBUG: API - Loading Task Status options form the backend.");
|
||||
const result = await this.request(FRAPPE_GET_TASKS_STATUS_OPTIONS, {});
|
||||
return result
|
||||
return result;
|
||||
}
|
||||
|
||||
static async setTaskStatus(taskName, newStatus) {
|
||||
return await this.request(FRAPPE_SET_TASK_STATUS_METHOD, { taskName, newStatus });
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@ -55,6 +55,7 @@
|
||||
:id="`isBilling-${index}`"
|
||||
v-model="address.isBillingAddress"
|
||||
:disabled="isSubmitting"
|
||||
@change="handleBillingChange(index)"
|
||||
style="margin-top: 0"
|
||||
/>
|
||||
<label :for="`isBilling-${index}`"><i class="pi pi-dollar" style="font-size: 0.75rem; margin-right: 0.25rem;"></i>Is Billing Address</label>
|
||||
@ -251,6 +252,36 @@ const formatAddressLine = (index, field, event) => {
|
||||
localFormData.value.addresses[index][field] = formatted;
|
||||
};
|
||||
|
||||
const handleBillingChange = (selectedIndex) => {
|
||||
// If the selected address is now checked as billing
|
||||
if (localFormData.value.addresses[selectedIndex].isBillingAddress) {
|
||||
// Uncheck all other addresses
|
||||
localFormData.value.addresses.forEach((addr, idx) => {
|
||||
if (idx !== selectedIndex) {
|
||||
addr.isBillingAddress = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-select all contacts
|
||||
if (contactOptions.value.length > 0) {
|
||||
localFormData.value.addresses[selectedIndex].contacts = contactOptions.value.map(
|
||||
(opt) => opt.value,
|
||||
);
|
||||
}
|
||||
|
||||
// Auto-select primary contact
|
||||
if (localFormData.value.contacts && localFormData.value.contacts.length > 0) {
|
||||
const primaryIndex = localFormData.value.contacts.findIndex((c) => c.isPrimary);
|
||||
if (primaryIndex !== -1) {
|
||||
localFormData.value.addresses[selectedIndex].primaryContact = primaryIndex;
|
||||
} else {
|
||||
// Fallback to first contact if no primary found
|
||||
localFormData.value.addresses[selectedIndex].primaryContact = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleZipcodeInput = async (index, event) => {
|
||||
const input = event.target.value;
|
||||
|
||||
@ -299,7 +330,7 @@ const handleZipcodeInput = async (index, event) => {
|
||||
.form-section {
|
||||
background: var(--surface-card);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--surface-border);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
transition: box-shadow 0.2s ease;
|
||||
@ -313,7 +344,7 @@ const handleZipcodeInput = async (index, event) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--surface-border);
|
||||
}
|
||||
|
||||
@ -254,7 +254,7 @@ defineExpose({
|
||||
.form-section {
|
||||
background: var(--surface-card);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--surface-border);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
transition: box-shadow 0.2s ease;
|
||||
@ -268,7 +268,7 @@ defineExpose({
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--surface-border);
|
||||
}
|
||||
|
||||
@ -285,7 +285,7 @@ defineExpose({});
|
||||
.form-section {
|
||||
background: var(--surface-card);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--surface-border);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
transition: box-shadow 0.2s ease;
|
||||
@ -299,7 +299,7 @@ defineExpose({});
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--surface-border);
|
||||
}
|
||||
|
||||
@ -1074,8 +1074,9 @@ const handleCancel = () => {
|
||||
|
||||
.status-cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
|
||||
@ -50,19 +50,30 @@
|
||||
placeholder="Select a contact"
|
||||
:disabled="!formData.address || !isEditable"
|
||||
fluid
|
||||
/>
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<div class="contact-option">
|
||||
<div class="contact-name">{{ slotProps.option.label }}</div>
|
||||
<div class="contact-detail" v-if="slotProps.option.email">{{ slotProps.option.email }}</div>
|
||||
<div class="contact-detail" v-if="slotProps.option.phone">{{ slotProps.option.phone }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</Select>
|
||||
<div v-if="selectedContact" class="verification-info">
|
||||
<strong>Email:</strong> {{ selectedContact.emailId || "N/A" }} <br />
|
||||
<strong>Phone:</strong> {{ selectedContact.phone || "N/A" }} <br />
|
||||
<strong>Primary Contact:</strong>
|
||||
{{ selectedContact.isPrimaryContact ? "Yes" : "No" }}
|
||||
{{ selectedAddress?.primaryContact === selectedContact.name ? "Yes" : "No" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template Section -->
|
||||
<div class="template-section">
|
||||
<div v-if="isNew">
|
||||
<label for="template" class="field-label">From Template</label>
|
||||
<label for="template" class="field-label">
|
||||
From Template
|
||||
<i class="pi pi-question-circle help-icon" v-tooltip.right="'Pre-fills estimate items and sets default Project Template. Serves as a starting point for this estimate.'"></i>
|
||||
</label>
|
||||
<div class="template-input-group">
|
||||
<Select
|
||||
v-model="selectedTemplate"
|
||||
@ -99,6 +110,7 @@
|
||||
<label for="projectTemplate" class="field-label">
|
||||
Project Template
|
||||
<span class="required">*</span>
|
||||
<i class="pi pi-question-circle help-icon" v-tooltip.right="'Used when generating a Project from this estimate. Defines tasks and default settings for the new Project.'"></i>
|
||||
</label>
|
||||
<Select
|
||||
v-model="formData.projectTemplate"
|
||||
@ -122,57 +134,63 @@
|
||||
/>
|
||||
<div v-for="(item, index) in selectedItems" :key="item.itemCode" class="item-row">
|
||||
<span>{{ item.itemName }}</span>
|
||||
<InputNumber
|
||||
v-model="item.qty"
|
||||
:min="1"
|
||||
:disabled="!isEditable"
|
||||
showButtons
|
||||
buttonLayout="horizontal"
|
||||
@input="onQtyChange(item)"
|
||||
class="qty-input"
|
||||
/>
|
||||
<div class="input-wrapper">
|
||||
<span class="input-label">Quantity</span>
|
||||
<InputNumber
|
||||
v-model="item.qty"
|
||||
:min="1"
|
||||
:disabled="!isEditable"
|
||||
showButtons
|
||||
buttonLayout="horizontal"
|
||||
@input="onQtyChange(item)"
|
||||
class="qty-input"
|
||||
/>
|
||||
</div>
|
||||
<span>Price: ${{ (item.standardRate || 0).toFixed(2) }}</span>
|
||||
<div class="discount-container">
|
||||
<div class="discount-input-wrapper">
|
||||
<InputNumber
|
||||
v-if="item.discountType === 'currency'"
|
||||
v-model="item.discountAmount"
|
||||
mode="currency"
|
||||
currency="USD"
|
||||
locale="en-US"
|
||||
:min="0"
|
||||
:disabled="!isEditable"
|
||||
@input="updateDiscountFromAmount(item)"
|
||||
placeholder="$0.00"
|
||||
class="discount-input"
|
||||
/>
|
||||
<InputNumber
|
||||
v-else
|
||||
v-model="item.discountPercentage"
|
||||
suffix="%"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:disabled="!isEditable"
|
||||
@input="updateDiscountFromPercentage(item)"
|
||||
placeholder="0%"
|
||||
class="discount-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="discount-toggle">
|
||||
<Button
|
||||
icon="pi pi-dollar"
|
||||
class="p-button-sm p-button-outlined"
|
||||
:class="{ 'p-button-secondary': item.discountType !== 'currency' }"
|
||||
@click="toggleDiscountType(item, 'currency')"
|
||||
:disabled="!isEditable"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-percentage"
|
||||
class="p-button-sm p-button-outlined"
|
||||
:class="{ 'p-button-secondary': item.discountType !== 'percentage' }"
|
||||
@click="toggleDiscountType(item, 'percentage')"
|
||||
:disabled="!isEditable"
|
||||
/>
|
||||
<div class="input-wrapper">
|
||||
<span class="input-label">Discount</span>
|
||||
<div class="discount-container">
|
||||
<div class="discount-input-wrapper">
|
||||
<InputNumber
|
||||
v-if="item.discountType === 'currency'"
|
||||
v-model="item.discountAmount"
|
||||
mode="currency"
|
||||
currency="USD"
|
||||
locale="en-US"
|
||||
:min="0"
|
||||
:disabled="!isEditable"
|
||||
@input="updateDiscountFromAmount(item)"
|
||||
placeholder="$0.00"
|
||||
class="discount-input"
|
||||
/>
|
||||
<InputNumber
|
||||
v-else
|
||||
v-model="item.discountPercentage"
|
||||
suffix="%"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:disabled="!isEditable"
|
||||
@input="updateDiscountFromPercentage(item)"
|
||||
placeholder="0%"
|
||||
class="discount-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="discount-toggle">
|
||||
<Button
|
||||
icon="pi pi-dollar"
|
||||
class="p-button-sm p-button-outlined"
|
||||
:class="{ 'p-button-secondary': item.discountType !== 'currency' }"
|
||||
@click="toggleDiscountType(item, 'currency')"
|
||||
:disabled="!isEditable"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-percentage"
|
||||
class="p-button-sm p-button-outlined"
|
||||
:class="{ 'p-button-secondary': item.discountType !== 'percentage' }"
|
||||
@click="toggleDiscountType(item, 'percentage')"
|
||||
:disabled="!isEditable"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span>Total: ${{ ((item.qty || 0) * (item.standardRate || 0) - (item.discountAmount || 0)).toFixed(2) }}</span>
|
||||
@ -202,7 +220,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div v-if="estimate">
|
||||
<Button label="Send Estimate" @click="showConfirmationModal = true" :disabled="estimate.customSent === 1"/>
|
||||
<Button label="Send Estimate" @click="initiateSendEstimate" :disabled="estimate.customSent === 1"/>
|
||||
</div>
|
||||
<div v-if="estimate && estimate.customSent === 1" class="response-status">
|
||||
<h4>Customer Response:</h4>
|
||||
@ -296,6 +314,27 @@
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<!-- Down Payment Warning Modal -->
|
||||
<Modal
|
||||
:visible="showDownPaymentWarningModal"
|
||||
@update:visible="showDownPaymentWarningModal = $event"
|
||||
@close="showDownPaymentWarningModal = false"
|
||||
:options="{ showActions: false }"
|
||||
>
|
||||
<template #title>Warning</template>
|
||||
<div class="modal-content">
|
||||
<p>Down payment is not required for this estimate. Ok to proceed?</p>
|
||||
<div class="confirmation-buttons">
|
||||
<Button
|
||||
label="No"
|
||||
@click="showDownPaymentWarningModal = false"
|
||||
severity="secondary"
|
||||
/>
|
||||
<Button label="Yes" @click="proceedFromWarning" />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<!-- Confirmation Modal -->
|
||||
<Modal
|
||||
:visible="showConfirmationModal"
|
||||
@ -356,6 +395,7 @@ import InputText from "primevue/inputtext";
|
||||
import InputNumber from "primevue/inputnumber";
|
||||
import Button from "primevue/button";
|
||||
import Select from "primevue/select";
|
||||
import Tooltip from "primevue/tooltip";
|
||||
import Api from "../../api";
|
||||
import DataUtils from "../../utils";
|
||||
import { useLoadingStore } from "../../stores/loading";
|
||||
@ -367,6 +407,7 @@ const router = useRouter();
|
||||
const loadingStore = useLoadingStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
const company = useCompanyStore();
|
||||
const vTooltip = Tooltip;
|
||||
|
||||
const addressQuery = computed(() => route.query.address || "");
|
||||
const nameQuery = computed(() => route.query.name || "");
|
||||
@ -406,6 +447,7 @@ const selectedTemplate = ref(null);
|
||||
const showAddressModal = ref(false);
|
||||
const showAddItemModal = ref(false);
|
||||
const showConfirmationModal = ref(false);
|
||||
const showDownPaymentWarningModal = ref(false);
|
||||
const showResponseModal = ref(false);
|
||||
const showSaveTemplateModal = ref(false);
|
||||
const addressSearchResults = ref([]);
|
||||
@ -459,10 +501,20 @@ const fetchTemplates = async () => {
|
||||
const templateParam = route.query.template;
|
||||
if (templateParam) {
|
||||
console.log("DEBUG: Setting template from query param:", templateParam);
|
||||
console.log("DEBUG: Available templates:", templates.value.map(t => t.name));
|
||||
selectedTemplate.value = templateParam;
|
||||
// Trigger template change to load items and project template
|
||||
onTemplateChange();
|
||||
|
||||
// Find template by name (ID) or templateName (Label)
|
||||
const matchedTemplate = templates.value.find(t =>
|
||||
t.name === templateParam || t.templateName === templateParam
|
||||
);
|
||||
|
||||
if (matchedTemplate) {
|
||||
console.log("DEBUG: Found matched template:", matchedTemplate);
|
||||
selectedTemplate.value = matchedTemplate.name;
|
||||
// Trigger template change to load items and project template
|
||||
onTemplateChange();
|
||||
} else {
|
||||
console.log("DEBUG: No matching template found for param:", templateParam);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching templates:", error);
|
||||
@ -580,8 +632,10 @@ const selectAddress = async (address) => {
|
||||
contactOptions.value = contacts.value.map((c) => ({
|
||||
label: `${c.firstName || ""} ${c.lastName || ""}`.trim() || c.name,
|
||||
value: c.name,
|
||||
email: c.emailId,
|
||||
phone: c.phone || c.mobileNo
|
||||
}));
|
||||
const primary = contacts.value.find((c) => c.isPrimaryContact);
|
||||
const primary = contacts.value.find((c) => c.name === selectedAddress.value.primaryContact);
|
||||
console.log("DEBUG: Selected address contacts:", contacts.value);
|
||||
const existingContactName = estimate.value ? contacts.value.find((c) => c.fullName === estimate.value.partyName)?.name || "" : null;
|
||||
// Check for contact query param, then existing contact, then primary, then first contact
|
||||
@ -721,6 +775,19 @@ const getResponseText = (response) => {
|
||||
return "No response yet";
|
||||
};
|
||||
|
||||
const initiateSendEstimate = () => {
|
||||
if (!formData.requiresHalfPayment) {
|
||||
showDownPaymentWarningModal.value = true;
|
||||
} else {
|
||||
showConfirmationModal.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const proceedFromWarning = () => {
|
||||
showDownPaymentWarningModal.value = false;
|
||||
showConfirmationModal.value = true;
|
||||
};
|
||||
|
||||
const confirmAndSendEstimate = async () => {
|
||||
loadingStore.setLoading(true, "Sending estimate...");
|
||||
const updatedEstimate = await Api.sendEstimateEmail(estimate.value.name);
|
||||
@ -1265,5 +1332,38 @@ onMounted(async () => {
|
||||
.field-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.help-icon {
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: #2196f3;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.contact-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.contact-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.contact-detail {
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
<parameter name="filePath"></parameter>
|
||||
|
||||
@ -37,6 +37,7 @@
|
||||
<DataTable
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:tableActions="tableActions"
|
||||
tableName="jobtasks"
|
||||
:lazy="true"
|
||||
:totalRecords="totalRecords"
|
||||
@ -80,9 +81,49 @@ const columns = [
|
||||
{ label: "ID", fieldName: "id", type: "text", sortable: true, filterable: true },
|
||||
{ label: "Address", fieldname: "address", type: "text" },
|
||||
{ label: "Category", fieldName: "", type: "text", sortable: true, filterable: true },
|
||||
{ label: "Status", fieldName: "status", type: "text", sortable: true, filterable: true },
|
||||
{ label: "Status", fieldName: "status", type: "status", sortable: true, filterable: true },
|
||||
];
|
||||
|
||||
const statusOptions = ref([
|
||||
"Open",
|
||||
"Working",
|
||||
"Pending Review",
|
||||
"Overdue",
|
||||
"Completed",
|
||||
"Cancelled",
|
||||
]);
|
||||
|
||||
const tableActions = computed(() => [
|
||||
{
|
||||
label: "Set Status",
|
||||
rowAction: true,
|
||||
type: "menu",
|
||||
menuItems: statusOptions.value.map((option) => ({
|
||||
label: option,
|
||||
command: async (rowData) => {
|
||||
console.log("Setting status for row:", rowData, "to:", option);
|
||||
try {
|
||||
await Api.setTaskStatus(rowData.id, option);
|
||||
|
||||
// Find and update the row in the table data
|
||||
const rowIndex = tableData.value.findIndex((row) => row.id === rowData.id);
|
||||
if (rowIndex >= 0) {
|
||||
// Update reactively
|
||||
tableData.value[rowIndex].status = option;
|
||||
notifications.addSuccess(`Status updated to ${option}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating status:", error);
|
||||
notifications.addError("Failed to update status");
|
||||
}
|
||||
},
|
||||
})),
|
||||
layout: {
|
||||
priority: "menu",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const handleLazyLoad = async (event) => {
|
||||
console.log("Task list on Job Page - handling lazy load:", event);
|
||||
try {
|
||||
@ -191,6 +232,16 @@ const handleLazyLoad = async (event) => {
|
||||
|
||||
onMounted(async () => {
|
||||
console.log("DEBUG: Query params:", route.query);
|
||||
|
||||
try {
|
||||
const optionsResult = await Api.getTaskStatusOptions();
|
||||
if (optionsResult && optionsResult.length > 0) {
|
||||
statusOptions.value = optionsResult;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading task status options:", error);
|
||||
}
|
||||
|
||||
if (jobIdQuery.value) {
|
||||
// Viewing existing Job
|
||||
try {
|
||||
|
||||
@ -40,8 +40,7 @@ const showCompleted = ref(false);
|
||||
const statusOptions = ref([
|
||||
"Open",
|
||||
"Working",
|
||||
"Pending",
|
||||
"Review",
|
||||
"Pending Review",
|
||||
"Overdue",
|
||||
"Completed",
|
||||
"Cancelled",
|
||||
@ -92,7 +91,7 @@ const tableActions = [
|
||||
console.log("Setting status for row:", rowData, "to:", option);
|
||||
try {
|
||||
// Uncomment when API is ready
|
||||
// await Api.setTaskStatus(rowData.id, option);
|
||||
await Api.setTaskStatus(rowData.id, option);
|
||||
|
||||
// Find and update the row in the table data
|
||||
const rowIndex = tableData.value.findIndex(row => row.id === rowData.id);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user