build meeting notes form in install on migrate

This commit is contained in:
Casey 2026-01-27 11:40:49 -06:00
parent 6cd3d138ad
commit c024e7fd86
12 changed files with 227 additions and 124 deletions

View file

@ -445,12 +445,16 @@ def upsert_client(data):
address_docs = []
for address in addresses:
is_billing = True if address.get("is_billing_address") else False
is_service = True if address.get("is_service_address") else False
print("#####DEBUG: Creating address with data:", address)
address_doc = AddressService.create_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",
"custom_billing_address": is_billing,
"is_service_address": is_service,
"is_primary_address": is_billing,
"city": address.get("city"),
"state": address.get("state"),
"country": "United States",
@ -480,13 +484,12 @@ def upsert_client(data):
"address": address_doc.name
})
client_doc.save(ignore_permissions=True)
client_dict = client_doc.as_dict()
client_dict["contacts"] = [contact.as_dict() for contact in contact_docs]
client_dict["addresses"] = [address.as_dict() for address in address_docs]
frappe.local.message_log = []
return build_success_response({
"customer": client_doc.as_dict(),
"address": [address_doc.as_dict() for address_doc in address_docs],
"contacts": [contact_doc.as_dict() for contact_doc in contact_docs]
})
return build_success_response(client_dict)
except frappe.ValidationError as ve:
return build_error_response(str(ve), 400)
except Exception as e:

View file

@ -485,7 +485,7 @@ def upsert_estimate(data):
"letter_head": data.get("company"),
"custom_project_template": data.get("project_template", None),
"custom_quotation_template": data.get("quotation_template", None),
"from_onsite_meeting": data.get("onsite_meeting", None)
"from_onsite_meeting": data.get("from_onsite_meeting", None)
})
for item in data.get("items", []):
item = json.loads(item) if isinstance(item, str) else item

View file

@ -12,7 +12,31 @@ def get_service_appointments(companies, filters={}):
filters = json.loads(filters)
filters["company"] = ["in", companies]
service_appointment_names = frappe.get_all(
"Service Appointment",
"Service Address 2",
filters=filters,
pluck="name"
)
service_appointments = [
ServiceAppointmentService.get_full_dict(name)
for name in service_appointment_names
]
return build_success_response(service_appointments)
except Exception as e:
return build_error_response(str(e), 500)
@frappe.whitelist()
def get_unscheduled_service_appointments(companies):
"""Get unscheduled Service Appointments for given companies."""
try:
if isinstance(companies, str):
companies = json.loads(companies)
filters = {
"company": ["in", companies],
"expected_start_date": None,
"status": "Open"
}
service_appointment_names = frappe.get_all(
"Service Address 2",
filters=filters,
pluck="name"
)
@ -27,9 +51,11 @@ def get_service_appointments(companies, filters={}):
@frappe.whitelist()
def update_service_appointment_scheduled_dates(service_appointment_name: str, start_date, end_date, crew_lead_name, start_time=None, end_time=None):
"""Update scheduled dates for a Service Appointment."""
print(f"DEBUG: Updating scheduled dates for Service Appointment {service_appointment_name} to start: {start_date}, end: {end_date}, crew lead: {crew_lead_name}, start time: {start_time}, end time: {end_time}")
try:
updated_service_appointment = ServiceAppointmentService.update_scheduled_dates(
service_appointment_name,
crew_lead_name,
start_date,
end_date,
start_time,

View file

@ -58,7 +58,7 @@ def on_update_after_submit(doc, method):
print("DEBUG: Quotation marked as Won, updating current status.")
if doc.customer_type == "Lead":
print("DEBUG: Customer is a Lead, converting to Customer and updating Quotation.")
new_customer = ClientService.convert_lead_to_customer(doc.actual_customer_name, update_quotations=False)
new_customer = ClientService.convert_lead_to_customer(doc.actual_customer_name)
doc.actual_customer_name = new_customer.name
doc.customer_type = "Customer"
new_customer.reload()

View file

@ -17,11 +17,11 @@ def after_insert(doc, method):
)
if doc.project_template == "SNW Install":
print("DEBUG: Project template is SNW Install, creating Service Appointment")
# AddressService.update_value(
# doc.job_address,
# "job_status",
# "In Progress"
# )
AddressService.update_value(
doc.job_address,
"job_status",
"In Progress"
)
try:
service_apt = ServiceAppointmentService.create({
"project": doc.name,
@ -50,7 +50,7 @@ def after_insert(doc, method):
)
if task_names:
doc.save(ignore_permissions=True)
TaskService.calculate_and_set_due_dates(task_names, "Created", "Project")
TaskService.calculate_and_set_due_dates(task_names, "Created", current_triggering_dict=doc.as_dict())
@ -69,16 +69,16 @@ def before_save(doc, method):
elif not doc.expected_start_date or not doc.expected_end_date:
print("DEBUG: Project missing expected start or end date, marking as unscheduled")
doc.is_scheduled = 0
def after_save(doc, method):
print("DEBUG: After Save Triggered for Project:", doc.name)
event = TaskService.determine_event(doc)
if event:
TaskService.calculate_and_set_due_dates(
[task.task for task in doc.tasks],
event,
"Project"
current_triggering_dict=doc.as_dict()
)
def after_save(doc, method):
print("DEBUG: After Save Triggered for Project:", doc.name)
if doc.project_template == "SNW Install":
print("DEBUG: Project template is SNW Install, updating Address Job Status based on Project status")
status_mapping = {

View file

@ -4,14 +4,28 @@ from custom_ui.services import TaskService
def on_update(doc, method):
print("DEBUG: On Update Triggered for Service Appointment")
event = TaskService.determine_event(doc)
if event:
tasks = TaskService.get_tasks_by_project(doc.project)
task_names = [task.name for task in tasks]
TaskService.calculate_and_set_due_dates(task_names=task_names, event=event, triggering_doctype="Service Address 2")
# event = TaskService.determine_event(doc)
# if event:
# tasks = TaskService.get_tasks_by_project(doc.project)
# task_names = [task.name for task in tasks]
# TaskService.calculate_and_set_due_dates(task_names=task_names, event=event, triggering_doctype="Service Address 2")
def after_insert(doc, method):
print("DEBUG: After Insert Triggered for Service Appointment")
task_names = [task.name for task in TaskService.get_tasks_by_project(doc.project)]
TaskService.calculate_and_set_due_dates(task_names=task_names, event="Created", triggering_doctype="Service Address 2")
TaskService.calculate_and_set_due_dates(task_names=task_names, event="Created", current_triggering_dict=doc.as_dict())
def before_save(doc, method):
print("DEBUG: Before Save Triggered for Service Appointment")
if doc.status == "Open" and doc.expected_start_date:
doc.status = "Scheduled"
elif doc.status == "Scheduled" and not doc.expected_start_date:
doc.status = "Open"
if doc.status == "Scheduled" and doc.actual_start_date:
doc.status = "Started"
elif doc.status != "Completed" and doc.status != "Canceled" and doc.actual_end_date:
doc.status = "Completed"
event = TaskService.determine_event(doc)
if event:
task_names = [task.name for task in TaskService.get_tasks_by_project(doc.project)]
TaskService.calculate_and_set_due_dates(task_names=task_names, event=event, current_triggering_dict=doc.as_dict())

View file

@ -23,12 +23,12 @@ def after_insert(doc, method):
doc.customer, "tasks", {"task": doc.name, "project_template": doc.project_template }
)
task_names = [task.name for task in TaskService.get_tasks_by_project(doc.project)]
TaskService.calculate_and_set_due_dates(task_names, "Created", "Task")
TaskService.calculate_and_set_due_dates(task_names, "Created", current_triggering_dict=doc.as_dict())
def after_save(doc, method):
print("DEBUG: After Save Triggered for Task:", doc.name)
def before_save(doc, method):
print("DEBUG: Before Save Triggered for Task:", doc.name)
event = TaskService.determine_event(doc)
if event:
task_names = [task.name for task in TaskService.get_tasks_by_project(doc.project)]
TaskService.calculate_and_set_due_dates(task_names, event, "Task")
TaskService.calculate_and_set_due_dates(task_names, event, current_triggering_dict=doc.as_dict())

View file

@ -191,10 +191,16 @@ doc_events = {
},
"Task": {
"before_insert": "custom_ui.events.task.before_insert",
"after_insert": "custom_ui.events.task.after_insert"
"after_insert": "custom_ui.events.task.after_insert",
"before_save": "custom_ui.events.task.before_save"
},
"Bid Meeting Note Form": {
"after_insert": "custom_ui.events.general.attach_bid_note_form_to_project_template"
},
"Service Address 2": {
"before_save": "custom_ui.events.service_appointment.before_save",
"after_insert": "custom_ui.events.service_appointment.after_insert",
"on_update": "custom_ui.events.service_appointment.on_update"
}
}

View file

@ -41,7 +41,7 @@ def after_migrate():
# create_project_templates()
create_task_types()
# create_tasks()
# create_bid_meeting_note_form_templates()
create_bid_meeting_note_form_templates()
# update_address_fields()
# build_frontend()
@ -1252,85 +1252,117 @@ def create_project_templates():
]
}
import frappe
def create_bid_meeting_note_form_templates():
"""Create Bid Meeting Note Forms if they do not exist."""
print("\n🔧 Checking for Bid Meeting Note Forms...")
forms = {
"Sprinklers Northwest": [
{
"name": "SNW Install Bid Meeting Notes",
{
"title": "SNW Install Bid Meeting Notes",
"description": "Notes form for SNW Install bid meetings.",
"project_template": "SNW Install",
"fields": [{
"label": "Locate Needed",
"type": "Check",
"required": 1,
"help_text": "Indicate if a locate is needed for this project.",
"row": 1,
"column": 1
},
{
"label": "Permit Needed",
"type": "Check",
"required": 1,
"help_text": "Indicate if a permit is needed for this project.",
"row": 1,
"column": 2
},
{
"label": "Back Flow Test Required",
"type": "Check",
"required": 1,
"help_text": "Indicate if a backflow test is required after installation.",
"row": 1,
"column": 3
},
{
"label": "Machine Access",
"type": "Check",
"required": 1,
"row": 2,
"column": 1
},
{
"label": "Machines",
"type": "Multi-Select",
"options": "MT, Skip Steer, Excavator-E-50, Link Belt, Tre?, Forks, Auger, Backhoe, Loader, Duzer",
"required": 0,
"include_options": 1,
"conditional_on_field": "Machine Access",
"row": 2,
"column": 2
},
{
"label": "Materials Required",
"type": "Check",
"required": 1,
"row": 3,
"column": 0
},
{
"label": "Materials",
"type": "Multi-Select w/ Quantity",
"required": 0,
"doctype_for_select": "Item",
"conditional_on_field": "Materials Required",
"doctype_label_field": "itemName",
"row": 4,
"column": 0
}]
}]}
"fields": [
{
"label": "Locate Needed",
"type": "Check",
"required": 1,
"help_text": "Indicate if a locate is needed for this project.",
"row": 1,
"column": 1,
},
{
"label": "Permit Needed",
"type": "Check",
"required": 1,
"help_text": "Indicate if a permit is needed for this project.",
"row": 1,
"column": 2,
},
{
"label": "Back Flow Test Required",
"type": "Check",
"required": 1,
"help_text": "Indicate if a backflow test is required after installation.",
"row": 1,
"column": 3,
},
{
"label": "Machine Access",
"type": "Check",
"required": 1,
"row": 2,
"column": 1,
},
{
"label": "Machines",
"type": "Multi-Select",
"options": "MT, Skip Steer, Excavator-E-50, Link Belt, Tre?, Forks, Auger, Backhoe, Loader, Duzer",
"include_options": 1,
"conditional_on_field": "Machine Access",
"row": 2,
"column": 2,
},
{
"label": "Materials Required",
"type": "Check",
"required": 1,
"row": 3,
"column": 0,
},
{
"label": "Materials",
"type": "Multi-Select w/ Quantity",
"doctype_for_select": "Item",
"doctype_label_field": "itemName",
"conditional_on_field": "Materials Required",
"row": 4,
"column": 0,
},
],
}
]
}
for company, form_list in forms.items():
for form in form_list:
if frappe.db.exists("Bid Meeting Note Form", form["name"]):
# Idempotency check
if frappe.db.exists(
"Bid Meeting Note Form",
{"title": form["title"], "company": company},
):
continue
doc = frappe.get_doc({
"doctype": "Bid Meeting Note Form",
"company": company,
"title": form["title"],
"description": form["description"],
"project_template": form["project_template"],
"fields": form["fields"]
})
doc = frappe.new_doc("Bid Meeting Note Form")
doc.company = company
doc.title = form["title"]
doc.description = form.get("description")
doc.project_template = form.get("project_template")
for idx, field in enumerate(form.get("fields", []), start=1):
doc.append(
"fields",
{
"label": field["label"],
"type": field["type"],
"options": field.get("options"),
"required": field.get("required", 0),
"default_value": field.get("default_value"),
"read_only": field.get("read_only", 0),
"order": field.get("order", 0),
"help_text": field.get("help_text"),
"doctype_for_select": field.get("doctype_for_select"),
"include_options": field.get("include_options", 0),
"conditional_on_field": field.get("conditional_on_field"),
"conditional_on_value": field.get("conditional_on_value"),
"doctype_label_field": field.get("doctype_label_field"),
"row": field.get("row"),
"column": field.get("column"),
"idx": idx,
},
)
doc.insert(ignore_permissions=True)

View file

@ -122,7 +122,6 @@ class ClientService:
print(f"DEBUG: Linked quotation {quotation.get('quotation')} to customer")
except Exception as e:
print(f"ERROR: Failed to link quotation {quotation.get('quotation')}: {str(e)}")
frappe.log_error(f"Quotation linking error: {str(e)}", "convert_lead_to_customer")
if update_onsite_meetings:
print(f"DEBUG: Updating onsite meetings. Count: {len(lead_doc.get('onsite_meetings', []))}")

View file

@ -22,17 +22,18 @@ class ServiceAppointmentService:
service_appointment = frappe.get_doc("Service Address 2", service_appointment_name).as_dict()
service_appointment["service_address"] = AddressService.get_or_throw(service_appointment["service_address"]).as_dict()
service_appointment["customer"] = ClientService.get_client_or_throw(service_appointment["customer"]).as_dict()
service_appointment["project"] = DbService.get_doc_or_throw("Project", service_appointment["project"]).as_dict()
service_appointment["project"] = DbService.get_or_throw("Project", service_appointment["project"]).as_dict()
return service_appointment
@staticmethod
def update_scheduled_dates(service_appointment_name: str, start_date, end_date, start_time=None, end_time=None):
def update_scheduled_dates(service_appointment_name: str, crew_lead_name: str,start_date, end_date, start_time=None, end_time=None):
"""Update the scheduled start and end dates of a Service Appointment."""
print(f"DEBUG: Updating scheduled dates for Service Appointment {service_appointment_name} to start: {start_date}, end: {end_date}")
service_appointment = DbService.get_or_throw("Service Address 2", service_appointment_name)
service_appointment.expected_start_date = start_date
service_appointment.expected_end_date = end_date
service_appointment.foreman = crew_lead_name
if start_time:
service_appointment.expected_start_time = start_time
if end_time:

View file

@ -1,14 +1,15 @@
import frappe
from frappe.utils.safe_exec import safe_eval
from datetime import timedelta, datetime, date
from frappe.utils import getdate
class TaskService:
@staticmethod
def calculate_and_set_due_dates(task_names: list[str], event: str, triggering_doctype: str):
def calculate_and_set_due_dates(task_names: list[str], event: str, current_triggering_dict=None):
"""Calculate the due date for a list of tasks based on their expected end dates."""
for task_name in task_names:
TaskService.check_and_update_task_due_date(task_name, event, triggering_doctype)
TaskService.check_and_update_task_due_date(task_name, event, current_triggering_dict)
@staticmethod
@ -19,14 +20,14 @@ class TaskService:
return tasks
@staticmethod
def check_and_update_task_due_date(task_name: str, event: str, triggering_doctype: str):
def check_and_update_task_due_date(task_name: str, event: str, current_triggering_dict=None):
"""Determine the triggering configuration for a given task."""
task_type_doc = TaskService.get_task_type_doc(task_name)
if task_type_doc.no_due_date:
print(f"DEBUG: Task {task_name} is marked as no due date, skipping calculation.")
return
if task_type_doc.triggering_doctype != triggering_doctype:
print(f"DEBUG: Task {task_name} triggering doctype {task_type_doc.triggering_doctype} does not match triggering doctype {triggering_doctype}, skipping calculation.")
if task_type_doc.triggering_doctype != current_triggering_dict.get("doctype") and current_triggering_dict:
print(f"DEBUG: Task {task_name} triggering doctype {task_type_doc.triggering_doctype} does not match triggering doctype {current_triggering_dict.get('doctype')}, skipping calculation.")
return
if task_type_doc.trigger != event:
print(f"DEBUG: Task {task_name} trigger {task_type_doc.trigger} does not match event {event}, skipping calculation.")
@ -42,7 +43,7 @@ class TaskService:
print(f"DEBUG: Calculating triggering data for Task {task_name} from {calculate_from} on trigger {trigger}")
triggering_doc_dict = TaskService.get_triggering_doc_dict(task_name=task_name, task_type_doc=task_type_doc)
triggering_doc_dict = current_triggering_dict if current_triggering_dict else TaskService.get_triggering_doc_dict(task_name=task_name, task_type_doc=task_type_doc)
calculated_due_date, calculated_start_date = TaskService.calculate_dates(
@ -72,15 +73,18 @@ class TaskService:
def calculate_dates(task_name: str, triggering_doc_dict: dict, task_type_doc) -> tuple[date | None, date | None]:
offset_direction = task_type_doc.offset_direction
offset_days = task_type_doc.offset_days
base_date_field = TaskService.map_base_date_to_field(task_type_doc.base_date)
base_date_field = TaskService.map_base_date_to_field(task_type_doc.base_date, task_type_doc.triggering_doctype)
print(f"DEBUG: base_date_field for Task {task_name} is {base_date_field}")
if offset_direction == "Before":
offset_days = -offset_days
base_date_field_value = triggering_doc_dict.get(base_date_field)
print(f"DEBUG: base_date_field_value for Task {task_name} is {base_date_field_value}")
if isinstance(base_date_field_value, datetime):
base_date_field_value = base_date_field_value.date()
base_date_field_value = base_date_field_value
else:
base_date_field_value = getdate(base_date_field_value)
calculated_due_date = base_date_field_value + timedelta(days=offset_days)
calculated_start_date = None
@ -91,8 +95,8 @@ class TaskService:
@staticmethod
def determine_update_required(task_name: str, calculated_due_date: date | None, calculated_start_date: date | None) -> bool:
current_due_date = frappe.get_value("Task", task_name, "expected_end_date")
current_start_date = frappe.get_value("Task", task_name, "expected_start_date")
current_due_date = frappe.get_value("Task", task_name, "exp_end_date")
current_start_date = frappe.get_value("Task", task_name, "exp_start_date")
if current_due_date != calculated_due_date or current_start_date != calculated_start_date:
print(f"DEBUG: Update required for Task {task_name}. Current due date: {current_due_date}, Calculated due date: {calculated_due_date}. Current start date: {current_start_date}, Calculated start date: {calculated_start_date}")
return True
@ -104,30 +108,31 @@ class TaskService:
@staticmethod
def get_triggering_doc_dict(task_name: str, task_type_doc) -> dict | None:
project_name = frappe.get_value("Task", task_name, "project")
print(f"DEBUG: Project name: {project_name}")
dict = None
if task_type_doc.calculate_from == "Project":
dict = frappe.get_doc("Project", project_name).to_dict()
dict = frappe.get_doc("Project", project_name).as_dict()
if task_type_doc.calculate_from == "Service Address 2":
service_name = frappe.get_value("Project", project_name, "service_appointment")
dict = frappe.get_doc("Service Address 2", service_name).to_dict()
dict = frappe.get_doc("Service Address 2", service_name).as_dict()
if task_type_doc.calculate_from == "Task":
project_doc = frappe.get_doc("Project", project_name)
for task in project_doc.tasks:
if task.task_type == task_type_doc.task_type_calculate_from:
dict = frappe.get_doc("Task", task.task).to_dict()
dict = frappe.get_doc("Task", task.task).as_dict()
print(f"DEBUG: Triggering doc dict for Task {task_name}: {dict}")
return dict
@staticmethod
def update_task_dates(task_name: str, calculated_due_date: date | None, calculated_start_date: date | None):
task_doc = frappe.get_doc("Task", task_name)
task_doc.expected_end_date = calculated_due_date
task_doc.expected_start_date = calculated_start_date
task_doc.exp_end_date = calculated_due_date
task_doc.exp_start_date = calculated_start_date
task_doc.save(ignore_permissions=True)
print(f"DEBUG: Updated Task {task_name} with new dates - Start: {calculated_start_date}, End: {calculated_due_date}")
@staticmethod
def map_base_date_to_field(base_date: str) -> str:
def map_base_date_to_field(base_date: str, triggering_doctype: str) -> str:
"""Map a base date configuration to a corresponding field name."""
base_date_field_map = {
"Start": "expected_start_date",
@ -135,12 +140,29 @@ class TaskService:
"Creation": "creation",
"Completion": "actual_end_date"
}
task_date_field_map = {
"Start": "exp_start_date",
"End": "exp_end_date",
"Creation": "creation",
"Completion": "actual_end_date"
}
if triggering_doctype == "Task":
return task_date_field_map.get(base_date, "exp_end_date")
return base_date_field_map.get(base_date, "expected_end_date")
@staticmethod
def determine_event(triggering_doc) -> str | None:
prev_doc = triggering_doc.get_doc_before_save()
if prev_doc.expected_end_date != triggering_doc.expected_end_date or prev_doc.expected_start_date != triggering_doc.expected_start_date:
def determine_event(triggering_doc) -> str | None:
print("DEBUG: Current Document:", triggering_doc.as_dict())
if not frappe.db.exists(triggering_doc.doctype, triggering_doc.name):
print("DEBUG: Document does not exist in database, returning None for event.")
return None
prev_doc = frappe.get_doc(triggering_doc.doctype, triggering_doc.name, as_dict=False, ignore_if_missing=True)
start_date_field = "expected_start_date" if triggering_doc.doctype != "Task" else "exp_start_date"
end_date_field = "expected_end_date" if triggering_doc.doctype != "Task" else "exp_end_date"
print("DEBUG: Previous Document:", prev_doc.as_dict() if prev_doc else "None")
if not prev_doc:
return None
if getattr(prev_doc, end_date_field) != getattr(triggering_doc, end_date_field) or getattr(prev_doc, start_date_field) != getattr(triggering_doc, start_date_field):
return "Scheduled"
elif prev_doc.status != triggering_doc.status and triggering_doc.status == "Completed":
return "Completed"