update with main

This commit is contained in:
Casey 2026-01-24 07:25:21 -06:00
parent 5e192a61e1
commit ba3e2a4d8e
29 changed files with 51749 additions and 139 deletions

View file

@ -26,6 +26,18 @@ def get_week_bid_meetings(week_start, week_end, company):
except Exception as e:
frappe.log_error(message=str(e), title="Get Week On-Site Meetings Failed")
return build_error_response(str(e), 500)
@frappe.whitelist()
def get_bid_meeting_note_form(project_template):
bid_meeting_note_form_name = frappe.db.get_value("Project Template", project_template, "bid_meeting_note_form")
if not bid_meeting_note_form_name:
return build_error_response(f"No Bid Meeting Note Form configured for Project Template '{project_template}'", 404)
try:
note_form = frappe.get_doc("Bid Meeting Note Form", bid_meeting_note_form_name)
return build_success_response(note_form.as_dict())
except Exception as e:
frappe.log_error(message=str(e), title="Get Bid Meeting Note Form Failed")
return build_error_response(str(e), 500)
@frappe.whitelist()
def get_bid_meetings(fields=["*"], filters={}, company=None):
@ -75,6 +87,61 @@ def get_unscheduled_bid_meetings(company):
frappe.log_error(message=str(e), title="Get Unscheduled On-Site Meetings Failed")
return build_error_response(str(e), 500)
@frappe.whitelist()
def submit_bid_meeting_note_form(bid_meeting, project_template, fields, form_template):
"""Submit Bid Meeting Note Form data for a specific On-Site Meeting."""
if isinstance(fields, str):
fields = json.loads(fields)
try:
print(f"DEBUG: Submitting Bid Meeting Note Form for meeting='{bid_meeting}' from template='{form_template}' with fields='{fields}'")
meeting = DbService.get_or_throw("On-Site Meeting", bid_meeting)
# Update fields on the meeting
meeting_note_field_docs = [{
"label": field.get("label"),
"type": field.get("type"),
"value": json.dumps(field.get("value")) if isinstance(field.get("value"), (list, dict)) else field.get("value"),
"row": field.get("row"),
"column": field.get("column"),
"value_doctype": field.get("doctype_for_select"),
"available_options": field.get("options"),
"include_available_options": field.get("include_available_options", False),
"conditional_on_field": field.get("conditional_on_field"),
"conditional_on_value": field.get("conditional_on_value"),
"doctype_label_field": field.get("doctype_label_field")
} for field in fields]
new_bid_meeting_note_doc = frappe.get_doc({
"doctype": "Bid Meeting Note",
"bid_meeting": bid_meeting,
"project_template": project_template,
"form_template": form_template,
"fields": meeting_note_field_docs
})
new_bid_meeting_note_doc.insert(ignore_permissions=True)
for field_row, field in zip(new_bid_meeting_note_doc.fields, fields):
if not isinstance(field.get("value"), list):
continue
for item in field["value"]:
if not isinstance(item, dict):
continue
new_bid_meeting_note_doc.append("quantities", {
"meeting_note_field": field_row.name,
"item": item.get("item"),
"quantity": item.get("quantity")
})
new_bid_meeting_note_doc.save(ignore_permissions=True)
meeting.bid_notes = new_bid_meeting_note_doc.name
meeting.status = "Completed"
meeting.save()
frappe.db.commit()
return build_success_response(meeting.as_dict())
except frappe.DoesNotExistError:
return build_error_response(f"On-Site Meeting '{bid_meeting}' does not exist.", 404)
except Exception as e:
frappe.log_error(message=str(e), title="Submit Bid Meeting Note Form Failed")
return build_error_response(str(e), 500)
@frappe.whitelist()
def get_bid_meeting(name):

View file

@ -14,8 +14,34 @@ from custom_ui.services import DbService, ClientService, AddressService, Contact
def get_estimate_table_data_v2(filters={}, sortings=[], page=1, page_size=10):
"""Get paginated estimate table data with filtering and sorting."""
print("DEBUG: Raw estimate options received:", filters, sortings, page, page_size)
filters, sortings, page, page_size = DbUtils.process_query_conditions(filters, sortings, page, page_size)
filters, sortings, page, page_size = DbUtils.process_datatable_request(filters, sortings, page, page_size)
sortings = "modified desc" if not sortings else sortings
count = frappe.db.count("Quotation", filters=filters)
print(f"DEBUG: Number of estimates returned: {count}")
estimate_names = frappe.db.get_all(
"Quotation",
filters=filters,
pluck="name",
limit=page_size,
start=(page) * page_size,
order_by=sortings
)
estimates = [frappe.get_doc("Quotation", name).as_dict() for name in estimate_names]
tableRows = []
for estimate in estimates:
tableRow = {
"id": estimate["name"],
"address": frappe.db.get_value("Address", estimate.get("custom_job_address"), "full_address"),
# strip "-#-" from actual_customer_name and anything that comes after it
"customer": estimate.get("actual_customer_name").split("-#-")[0] if estimate.get("actual_customer_name") else estimate.get("customer_name") if estimate.get("customer_name") else "",
"status": estimate.get("custom_current_status", ""),
"order_type": estimate.get("order_type", ""),
}
tableRows.append(tableRow)
table_data_dict = build_datatable_dict(data=tableRows, count=count, page=page, page_size=page_size)
return build_success_response(table_data_dict)
@frappe.whitelist()
def get_estimate_table_data(filters={}, sortings=[], page=1, page_size=10):

View file

@ -1,6 +1,7 @@
import frappe
from custom_ui.db_utils import build_history_entries, build_success_response, build_error_response
from datetime import datetime, timedelta
import json
def get_doc_history(doctype, docname):
"""Get the history of changes for a specific document."""
@ -76,4 +77,24 @@ def get_week_holidays(week_start_date: str):
)
print(f"DEBUG: Retrieved holidays from {start_date} to {end_date}: {holidays}")
return build_success_response(holidays)
return build_success_response(holidays)
@frappe.whitelist()
def get_doc_list(doctype, fields=["*"], filters={}, pluck=None):
"""Get list of documents for a given doctype with specified fields and filters."""
if isinstance(fields, str):
fields = json.loads(fields)
if isinstance(filters, str):
filters = json.loads(filters)
try:
docs = frappe.get_all(
doctype,
fields=fields,
filters=filters,
order_by="creation desc",
pluck=pluck
)
print(f"DEBUG: Retrieved documents for {doctype} with filters {filters}: {docs}")
return build_success_response(docs)
except Exception as e:
return build_error_response(str(e), 500)

View file

@ -1,6 +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
from custom_ui.services import AddressService, ClientService, ServiceAppointmentService
from frappe.utils import getdate
# ===============================================================================

View file

@ -0,0 +1,40 @@
import frappe, json
from custom_ui.db_utils import build_success_response, build_error_response
from custom_ui.services import ServiceAppointmentService
@frappe.whitelist()
def get_service_appointments(companies, filters={}):
"""Get Service Appointments for given companies."""
try:
if isinstance(companies, str):
companies = json.loads(companies)
if isinstance(filters, str):
filters = json.loads(filters)
filters["company"] = ["in", companies]
service_appointment_names = frappe.get_all(
"Service Appointment",
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 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."""
try:
updated_service_appointment = ServiceAppointmentService.update_scheduled_dates(
service_appointment_name,
start_date,
end_date,
start_time,
end_time
)
return build_success_response(updated_service_appointment.as_dict())
except Exception as e:
return build_error_response(str(e), 500)

View file

@ -0,0 +1,6 @@
import frappe
def attach_bid_note_form_to_project_template(doc, method):
"""Attatch Bid Meeting Note Form to Project Template on insert."""
print("DEBUG: Attaching Bid Meeting Note Form to Project Template")
frappe.set_value("Project Template", doc.project_template, "bid_meeting_note_form", doc.name)

View file

@ -1,5 +1,5 @@
import frappe
from custom_ui.services import AddressService, ClientService
from custom_ui.services import AddressService, ClientService, ServiceAppointmentService, TaskService
from datetime import timedelta
def after_insert(doc, method):
@ -21,6 +21,21 @@ def after_insert(doc, method):
"job_status",
"In Progress"
)
service_apt = ServiceAppointmentService.create({
"project": doc.name,
"customer": doc.customer,
"address": doc.job_address,
"company": doc.company,
"project_template": doc.project_template
})
frappe.db.set_value("Project", doc.name, "service_appointment", service_apt.name)
task_names = [task.name for task in TaskService.get_tasks_by_project(doc.name)]
for task_name in task_names:
doc.append("tasks", {
"task": task_name
})
TaskService.calculate_and_set_due_dates(task_names, "Created")
def before_insert(doc, method):
@ -41,6 +56,12 @@ def before_save(doc, method):
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
)
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

@ -0,0 +1,17 @@
import frappe
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)
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")

View file

@ -1,5 +1,5 @@
import frappe
from custom_ui.services import AddressService, ClientService
from custom_ui.services import AddressService, ClientService, TaskService
def before_insert(doc, method):
"""Set values before inserting a Task."""
@ -18,4 +18,16 @@ def after_insert(doc, method):
AddressService.append_link_v2(
doc.custom_property, "links", {"link_doctype": "Task", "link_name": doc.name}
)
ClientService.append_link_v2(
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")
def after_save(doc, method):
print("DEBUG: After 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)

View file

@ -0,0 +1,519 @@
[
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Opportunity",
"enabled": 1,
"modified": "2024-06-28 04:58:27.527685",
"module": null,
"name": "Set Item Price",
"script": "frappe.ui.form.on('Opportunity Item', {\r\n item_code: function(frm, cdt, cdn) {\r\n var row = locals[cdt][cdn];\r\n if (row.item_code) {\r\n frappe.call({\r\n method: 'frappe.client.get_value',\r\n args: {\r\n 'doctype': 'Item Price',\r\n 'filters': {\r\n 'item_code': row.item_code,\r\n 'selling': 1\r\n },\r\n 'fieldname': ['price_list_rate']\r\n },\r\n callback: function(r) {\r\n if (r.message) {\r\n frappe.model.set_value(cdt, cdn, 'rate', r.message.price_list_rate);\r\n frappe.model.set_value(cdt, cdn, 'amount', row.qty * r.message.price_list_rate);\r\n }\r\n }\r\n });\r\n }\r\n },\r\n qty: function(frm, cdt, cdn) {\r\n var row = locals[cdt][cdn];\r\n if (row.qty && row.rate) {\r\n frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.rate);\r\n }\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "SNW Jobs",
"enabled": 1,
"modified": "2024-07-18 02:56:08.345504",
"module": null,
"name": "SNW Jobs - auto populate days",
"script": "frappe.ui.form.on('SNW Jobs', {\n refresh(frm) {\n // your code here (if needed)\n },\n start_date(frm) {\n calculate_total_days(frm);\n },\n end_date(frm) {\n calculate_total_days(frm);\n }\n});\n\nfunction calculate_total_days(frm) {\n if (frm.doc.start_date && frm.doc.end_date) {\n const startDate = new Date(frm.doc.start_date);\n const endDate = new Date(frm.doc.end_date);\n const timeDiff = endDate - startDate;\n const dayDiff = timeDiff / (1000 * 3600 * 24);\n frm.set_value('number_of_days', dayDiff);\n }\n}",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "SNW Jobs",
"enabled": 1,
"modified": "2024-07-18 03:04:59.760299",
"module": null,
"name": "SNW Jobs - Calculate Balance",
"script": "frappe.ui.form.on('SNW Jobs', {\r\n refresh(frm) {\r\n // Your code here (if needed)\r\n },\r\n total_expected_price(frm) {\r\n calculate_balance(frm);\r\n },\r\n paid(frm) {\r\n calculate_balance(frm);\r\n }\r\n});\r\n\r\nfunction calculate_balance(frm) {\r\n if (frm.doc.total_expected_price != null && frm.doc.paid != null) {\r\n const balance = frm.doc.total_expected_price - frm.doc.paid;\r\n frm.set_value('balance', balance);\r\n }\r\n}\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "SNW Jobs",
"enabled": 1,
"modified": "2024-07-18 03:12:21.825901",
"module": null,
"name": "SNW Jobs - pull crew lead employees",
"script": "frappe.ui.form.on('SNW Jobs', {\n\trefresh(frm) {\n\t\t// your code here\n\t},\n\t onload(frm) {\n frm.set_query('crew_leader', function() {\n return {\n filters: {\n designation: 'Crew Lead'\n }\n };\n });\n }\n})",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Follow Up Checklist",
"enabled": 1,
"modified": "2024-09-16 05:58:34.623817",
"module": null,
"name": "auto populate follow up form",
"script": "frappe.ui.form.on('Follow Up Checklist', {\n customer:function(frm) {\n\t\t // Fetch customer details like phone and email\n frappe.db.get_value(\"Address\", {\"customer\": frm.doc.customer, \"customer_primary_address\": 1}, \"address_line1\", function(r) {\n if(r && r.address_line1) {\n frm.set_value(\"address\", r.address_line1); // Set the address field\n }\n });\n }\n});",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Quotation",
"enabled": 1,
"modified": "2025-04-24 05:36:48.042696",
"module": null,
"name": "Item Markup Script",
"script": "frappe.ui.form.on('Quotation Item', {\r\n item_code: function(frm, cdt, cdn) {\r\n let row = locals[cdt][cdn];\r\n if (row.item_code) {\r\n // Fetch the item price from the Item Price List\r\n frappe.call({\r\n method: 'frappe.client.get_list',\r\n args: {\r\n doctype: 'Item Price',\r\n filters: {\r\n item_code: row.item_code,\r\n price_list: frm.doc.selling_price_list // Assuming the price list is set in the Quotation\r\n },\r\n fields: ['price_list_rate']\r\n },\r\n callback: function(response) {\r\n if (response.message && response.message.length > 0) {\r\n // Get the price from the Item Price List\r\n let base_rate = response.message[0].price_list_rate || 0;\r\n\r\n // Fetch the markup percentage from the Item master\r\n frappe.call({\r\n method: 'frappe.client.get',\r\n args: {\r\n doctype: 'Item',\r\n name: row.item_code\r\n },\r\n callback: function(r) {\r\n if (r.message) {\r\n // Fetch the markup percentage from the Item master\r\n let markup = r.message.custom_markup_percentage || 0; // Default to 0% if not set\r\n \r\n // Calculate the new rate with markup\r\n let new_rate = base_rate + (base_rate * (markup / 100));\r\n frappe.model.set_value(cdt, cdn, 'rate', new_rate);\r\n \r\n // Refresh the items table to show the updated rate\r\n frm.refresh_field('items');\r\n }\r\n }\r\n });\r\n }\r\n }\r\n });\r\n }\r\n }\r\n});\r\n\r\n// Optional: Recalculate all items on form refresh or load\r\nfrappe.ui.form.on('Quotation', {\r\n refresh: function(frm) {\r\n frm.doc.items.forEach(function(item) {\r\n //frappe.model.trigger('item_code', item.name);\r\n });\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Quotation",
"enabled": 1,
"modified": "2025-01-08 05:04:26.743210",
"module": null,
"name": "Quotation - Set Same Valid Until Date",
"script": "frappe.ui.form.on(\"Quotation\", {\n onload: function(frm) {\n frm.set_value('valid_till', '2025-12-31');\n }\n});",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Note",
"enabled": 1,
"modified": "2024-10-24 23:43:27.548340",
"module": null,
"name": "Open Note in Edit Mode",
"script": "frappe.ui.form.on('Note', {\r\n onload_post_render: function(frm) {\r\n // Check if this is a new document or in read mode, then switch to edit mode\r\n if (frm.is_new() || frm.doc.__unsaved) {\r\n frm.page.set_primary_action(__('Save'), () => frm.save());\r\n frm.page.wrapper.find('.btn-primary').removeClass('hidden');\r\n frm.enable_save();\r\n }\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Address",
"enabled": 1,
"modified": "2024-12-05 11:55:14.179233",
"module": null,
"name": "Address Doctype Linked City Auto Populate",
"script": "frappe.ui.form.on('Address', {\r\n custom_linked_city: function (frm) {\r\n if (frm.doc.custom_linked_city) {\r\n frappe.db.get_doc('City', frm.doc.custom_linked_city).then(city_doc => {\r\n frm.set_value('city', city_doc.city_name); // Sync to mandatory City field\r\n frm.set_value('state', city_doc.state); // Populate State\r\n if (city_doc.zip_code) {\r\n frm.set_value('zip_code', city_doc.zip_code); // Populate Zip Code\r\n }\r\n });\r\n }\r\n },\r\n city: function (frm) {\r\n // Optionally, sync back to custom_linked_city when the mandatory City field is used\r\n if (!frm.doc.custom_linked_city && frm.doc.city) {\r\n frappe.msgprint(__('Consider selecting a linked city for better accuracy.'));\r\n }\r\n }\r\n});",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Address",
"enabled": 1,
"modified": "2024-12-06 05:15:31.487671",
"module": null,
"name": "Address - Irrigation District Visibility Condition",
"script": "frappe.ui.form.on('Address', {\r\n refresh: function(frm) {\r\n let show_irrigation_district = false;\r\n\r\n // Check if 'is_shipping_address' is checked\r\n if (frm.doc.is_shipping_address) {\r\n // Required companies list\r\n const required_companies = ['Sprinklers Northwest', 'Nuco Yard Care'];\r\n\r\n // Check child table rows\r\n if (frm.doc.custom_linked_companies) {\r\n show_irrigation_district = frm.doc.custom_linked_companies.some(row =>\r\n required_companies.includes(row.company)\r\n );\r\n }\r\n }\r\n\r\n // Show or hide the custom_irrigation_district field\r\n frm.set_df_property('custom_irrigation_district', 'hidden', !show_irrigation_district);\r\n },\r\n\r\n is_service_address: function(frm) {\r\n // Re-run visibility logic when 'is_service_address' changes\r\n frm.trigger('refresh');\r\n },\r\n\r\n custom_linked_companies: function(frm) {\r\n // Re-run visibility logic when the child table is updated\r\n frm.trigger('refresh');\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Address",
"enabled": 1,
"modified": "2024-12-06 05:27:19.840296",
"module": null,
"name": "Address - Installed By Snw Visibility Condition",
"script": "frappe.ui.form.on('Address', {\r\n refresh: function(frm) {\r\n frm.trigger('toggle_irrigation_fields');\r\n },\r\n\r\n custom_installed_by_sprinklers_nw: function(frm) {\r\n frm.trigger('toggle_irrigation_fields');\r\n },\r\n\r\n toggle_irrigation_fields: function(frm) {\r\n const is_installed = frm.doc.custom_installed_by_sprinklers_nw;\r\n\r\n // Toggle visibility for irrigation-related fields\r\n frm.set_df_property('custom_install_month', 'hidden', !is_installed);\r\n frm.set_df_property('custom_install_year', 'hidden', !is_installed);\r\n frm.set_df_property('custom_backflow_test_report', 'hidden', !is_installed);\r\n frm.set_df_property('custom_photo_attachment', 'hidden', !is_installed);\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Address",
"enabled": 0,
"modified": "2024-12-13 07:14:26.487456",
"module": null,
"name": "Address - Google Map Display",
"script": "frappe.ui.form.on('Address', {\r\n refresh: function(frm) {\r\n // Render Google Map if address fields are available and saved\r\n if (!frm.is_new() && frm.doc.address_line1 && frm.doc.city && frm.doc.country) {\r\n frm.trigger('render_google_map');\r\n }\r\n },\r\n\r\n render_google_map: function(frm) {\r\n // Construct the full address string\r\n const address = [\r\n frm.doc.address_line1,\r\n frm.doc.address_line2 || '', // Optional\r\n frm.doc.city,\r\n frm.doc.state || '', // Optional\r\n frm.doc.pincode || '', // Optional\r\n frm.doc.country\r\n ].filter(Boolean).join(', '); // Remove empty fields\r\n\r\n // Replace with your Google Maps API Key\r\n const apiKey = 'AIzaSyB2uNXSQpMp-lGJHIWFpzloWxs76zjkU8Y';\r\n\r\n // Generate the embed URL\r\n const mapUrl = `https://www.google.com/maps/embed/v1/place?key=${apiKey}&q=${encodeURIComponent(address)}`;\r\n\r\n // Render the iframe in the HTML field\r\n frm.fields_dict.custom_google_map.$wrapper.html(`\r\n <iframe \r\n width=\"100%\" \r\n height=\"400\" \r\n frameborder=\"0\" \r\n style=\"border:0\" \r\n src=\"${mapUrl}\" \r\n allowfullscreen>\r\n </iframe>\r\n `);\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Address",
"enabled": 0,
"modified": "2025-05-15 09:00:39.086280",
"module": null,
"name": "Address - Auto-Filling Mutiple Contact Email and Phone",
"script": "frappe.ui.form.on('Address Contact Role', {\r\n contact: function(frm, cdt, cdn) {\r\n let row = frappe.get_doc(cdt, cdn);\r\n\r\n if (row.contact) {\r\n // Fetch email and phone from the selected Contact\r\n frappe.db.get_doc('Contact', row.contact).then(contact => {\r\n if (contact) {\r\n // Pull primary email and phone from the Contact\r\n row.email = contact.email_id || '';\r\n row.phone = contact.phone || contact.mobile_no || ''; // Prefer phone, fallback to mobile_no\r\n frm.refresh_field('custom_linked_contacts');\r\n }\r\n });\r\n } else {\r\n // Clear fields if no Contact selected\r\n row.email = '';\r\n row.phone = '';\r\n frm.refresh_field('custom_linked_contacts');\r\n }\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Locate Log",
"enabled": 1,
"modified": "2024-12-11 12:11:35.789512",
"module": null,
"name": "Locate Button",
"script": "frappe.ui.form.on('Locate Log', {\r\n refresh: function(frm) {\r\n // Add the custom button \"Mark as Located\"\r\n if (frm.doc.status !== 'Completed') {\r\n frm.add_custom_button(__('Mark as Located'), function() {\r\n frm.trigger('mark_as_located');\r\n });\r\n }\r\n },\r\n mark_as_located: function(frm) {\r\n // Check if Dig Ticket # is provided\r\n if (!frm.doc.dig_ticket_number) {\r\n frappe.msgprint(__('Please enter a Dig Ticket #.'));\r\n return;\r\n }\r\n\r\n // Trigger the backend method to update the Job Queue and Locate Log\r\n frappe.call({\r\n method: 'my_app.my_module.api.update_locate_log_and_job_queue',\r\n args: {\r\n locate_log: frm.doc.name,\r\n dig_ticket_number: frm.doc.dig_ticket_number\r\n },\r\n callback: function(r) {\r\n if (r.message) {\r\n frappe.msgprint(__('Locate marked as completed.'));\r\n frm.reload_doc(); // Reload the form to reflect changes\r\n }\r\n }\r\n });\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Pre-Built Routes",
"enabled": 0,
"modified": "2024-12-11 13:24:02.894613",
"module": null,
"name": "Real-Time Updates for Address -Route Connection",
"script": "frappe.ui.form.on('Assigned Address', {\r\n address_name: function (frm, cdt, cdn) {\r\n const row = locals[cdt][cdn];\r\n\r\n // Ensure the address_name field is set\r\n if (row.address_name) {\r\n // Fetch the custom_confirmation_status from the Address Doctype\r\n frappe.db.get_value('Address', row.address_name, 'custom_confirmation_status', (value) => {\r\n if (value && value.custom_confirmation_status) {\r\n // Set the status in the child table to match the Address confirmation status\r\n frappe.model.set_value(cdt, cdn, 'status', value.custom_confirmation_status);\r\n } else {\r\n // Default to No Response if Address confirmation status is not set\r\n frappe.model.set_value(cdt, cdn, 'status', 'No Response');\r\n }\r\n });\r\n } else {\r\n // If no address_name is set, default status to No Response\r\n frappe.model.set_value(cdt, cdn, 'status', 'No Response');\r\n }\r\n }\r\n});\r\n\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Address",
"enabled": 0,
"modified": "2024-12-11 13:09:36.908372",
"module": null,
"name": "Real Time Updates for Address Doctype Connection to Pre-Built",
"script": "frappe.ui.form.on('Address', {\r\n custom_confirmation_status: function (frm) {\r\n // Check if the Service Route is set\r\n if (!frm.doc.custom_service_route) {\r\n frappe.msgprint(__('Service Route is not assigned for this address.'));\r\n return;\r\n }\r\n\r\n // Retrieve the Assigned Address row where this address is linked\r\n frappe.call({\r\n method: 'frappe.client.get_value',\r\n args: {\r\n doctype: 'Assigned Address',\r\n filters: { address_name: frm.doc.name },\r\n fieldname: 'name'\r\n },\r\n callback: function (response) {\r\n if (response.message) {\r\n const assigned_row_name = response.message.name;\r\n // Update the Status in the Assigned Address row\r\n frappe.call({\r\n method: 'frappe.client.set_value',\r\n args: {\r\n doctype: 'Assigned Address',\r\n name: assigned_row_name,\r\n fieldname: 'status',\r\n value: frm.doc.custom_confirmation_status\r\n },\r\n callback: function (response) {\r\n if (!response.exc) {\r\n frappe.msgprint(__('Status updated in Pre-Built Route.'));\r\n }\r\n }\r\n });\r\n } else {\r\n frappe.msgprint(__('This address is not linked to any Pre-Built Route.'));\r\n }\r\n }\r\n });\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Pre-Built Routes",
"enabled": 1,
"modified": "2024-12-13 09:14:52.635425",
"module": null,
"name": "Color Code Route Child Table",
"script": "frappe.ui.form.on('Pre-Built Routes', {\r\n refresh: function (frm) {\r\n // Loop through the rows in the Assigned Addresses table\r\n frm.fields_dict['assigned_addresses'].grid.wrapper.find('.grid-row').each(function () {\r\n const row = $(this);\r\n const doc = row.data('doc'); // Get the child table row data\r\n\r\n if (doc && doc.status) {\r\n // Apply color based on the Status field\r\n switch (doc.status) {\r\n case 'Confirmed':\r\n row.css('background-color', '#D4EDDA'); // Green for Scheduled\r\n break;\r\n case 'Reschedule':\r\n row.css('background-color', '#FFF3CD'); // Yellow for Completed\r\n break;\r\n case 'Declined':\r\n row.css('background-color', '#F8D7DA'); // Red for Pending Reschedule\r\n break;\r\n default:\r\n row.css('background-color', '#E2E0E0',); // Reset to default if no match\r\n break;\r\n }\r\n }\r\n });\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Pre-Built Routes",
"enabled": 1,
"modified": "2024-12-11 13:25:17.161414",
"module": null,
"name": "Dynamically Update Confirmation Status on Pre-Built Route",
"script": "frappe.ui.form.on('Assigned Address', {\r\n address_name: function (frm, cdt, cdn) {\r\n const row = locals[cdt][cdn];\r\n\r\n // Ensure the address_name field is set\r\n if (row.address_name) {\r\n // Fetch the custom_confirmation_status from the Address Doctype\r\n frappe.db.get_value('Address', row.address_name, 'custom_confirmation_status', (value) => {\r\n if (value && value.custom_confirmation_status) {\r\n // Set the status in the child table to match the Address confirmation status\r\n frappe.model.set_value(cdt, cdn, 'status', value.custom_confirmation_status);\r\n } else {\r\n // Default to No Response if Address confirmation status is not set\r\n frappe.model.set_value(cdt, cdn, 'status', 'No Response');\r\n }\r\n });\r\n } else {\r\n // If no address_name is set, default status to No Response\r\n frappe.model.set_value(cdt, cdn, 'status', 'No Response');\r\n }\r\n }\r\n});",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Pre-Built Routes",
"enabled": 0,
"modified": "2024-12-13 09:03:06.173211",
"module": null,
"name": "Route Map",
"script": "frappe.ui.form.on('Pre-Built Routes', {\r\n refresh: function (frm) {\r\n // Check if Google Maps API is loaded\r\n if (!window.google || !window.google.maps) {\r\n frappe.msgprint(__('Google Maps API is not loaded. Attempting to load...'));\r\n\r\n // Dynamically load the Google Maps API\r\n const script = document.createElement('script');\r\n script.src = `https://maps.googleapis.com/maps/api/js?key=AIzaSyB2uNXSQpMp-lGJHIWFpzloWxs76zjkU8Y`;\r\n script.async = true;\r\n script.defer = true;\r\n\r\n // Call renderMap after the API is loaded\r\n script.onload = function () {\r\n console.log(\"Google Maps API loaded successfully.\");\r\n renderMap(frm); // Render the map after API loads\r\n };\r\n\r\n // Handle API load failure\r\n script.onerror = function () {\r\n frappe.msgprint(__('Failed to load Google Maps API. Please check your API key.'));\r\n };\r\n\r\n document.head.appendChild(script);\r\n } else {\r\n // If API is already loaded, render the map immediately\r\n renderMap(frm);\r\n }\r\n }\r\n});\r\n\r\nfunction renderMap(frm) {\r\n // Ensure the form has assigned addresses\r\n const addresses = frm.doc.assigned_addresses || [];\r\n if (!addresses.length) {\r\n frappe.msgprint(__('No addresses found for this route.'));\r\n return;\r\n }\r\n\r\n // Prepare points for the map\r\n const points = addresses.map((row) => ({\r\n lat: parseFloat(row.latitude || 0),\r\n lng: parseFloat(row.longitude || 0),\r\n status: row.status || 'No Response',\r\n title: row.address_name || 'Unknown Address'\r\n }));\r\n\r\n console.log(\"Map points:\", points); // Debug the points array\r\n\r\n // Define marker colors based on status\r\n const statusColors = {\r\n 'Confirmed': 'green',\r\n 'Reschedule': 'yellow',\r\n 'Decline': 'red',\r\n 'No Response': 'white'\r\n };\r\n\r\n // Initialize map container\r\n const mapContainer = frm.fields_dict['route_map'].wrapper;\r\n mapContainer.innerHTML = '<div id=\"route-map\" style=\"height: 400px;\"></div>';\r\n const map = new google.maps.Map(document.getElementById('route-map'), {\r\n zoom: 10,\r\n mapTypeId: 'roadmap'\r\n });\r\n\r\n // Fit map bounds to all points\r\n const bounds = new google.maps.LatLngBounds();\r\n\r\n // Add markers for each point\r\n points.forEach((point) => {\r\n if (point.lat && point.lng) {\r\n const marker = new google.maps.Marker({\r\n position: { lat: point.lat, lng: point.lng },\r\n map: map,\r\n title: point.title,\r\n icon: {\r\n path: google.maps.SymbolPath.CIRCLE,\r\n scale: 8,\r\n fillColor: statusColors[point.status],\r\n fillOpacity: 1,\r\n strokeWeight: 0\r\n }\r\n });\r\n bounds.extend(marker.getPosition());\r\n }\r\n });\r\n\r\n // Adjust map view to fit all markers\r\n if (!bounds.isEmpty()) {\r\n map.fitBounds(bounds);\r\n } else {\r\n frappe.msgprint(__('No valid points to display on the map.'));\r\n }\r\n}\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Address",
"enabled": 1,
"modified": "2025-05-15 12:34:32.006687",
"module": null,
"name": "Test - Update Address Map Display with lat/long",
"script": "frappe.ui.form.on('Address', {\r\n refresh: function(frm) {\r\n frm.trigger('render_google_map');\r\n },\r\n\r\n render_google_map: function(frm) {\r\n if (!frm.is_new() && frm.doc.address_line1 && frm.doc.city && frm.doc.country) {\r\n // Render Google Map if address fields are available and saved\r\n\r\n const address = [\r\n frm.doc.address_line1,\r\n frm.doc.address_line2 || '',\r\n frm.doc.city,\r\n frm.doc.state || '',\r\n frm.doc.pincode || '',\r\n frm.doc.country\r\n ].filter(Boolean).join(', ');\r\n \r\n const apiKey = 'AIzaSyCd3ALZe6wjt3xnc7X_rRItfKAEJugfuZ4';\r\n const mapUrl = `https://www.google.com/maps/embed/v1/place?key=${apiKey}&q=${encodeURIComponent(address)}`;\r\n \r\n frm.fields_dict.custom_google_map.$wrapper.html(`\r\n <iframe \r\n width=\"100%\" \r\n height=\"400\" \r\n frameborder=\"0\" \r\n style=\"border:0\" \r\n src=\"${mapUrl}\" \r\n allowfullscreen>\r\n </iframe>\r\n `);\r\n } else {\r\n frm.fields_dict.custom_google_map.$wrapper.html('');\r\n }\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Pre-Built Routes",
"enabled": 1,
"modified": "2024-12-13 08:47:49.392360",
"module": null,
"name": "Test - Pull The lat/long into Assigned Address",
"script": "frappe.ui.form.on('Assigned Address', {\r\n address_name: function(frm, cdt, cdn) {\r\n let row = locals[cdt][cdn];\r\n if (row.address_name) {\r\n frappe.db.get_value('Address', row.address_name, ['custom_latitude', 'custom_longitude'], (value) => {\r\n if (value) {\r\n frappe.model.set_value(cdt, cdn, 'latitude', value.custom_latitude);\r\n frappe.model.set_value(cdt, cdn, 'longitude', value.custom_longitude);\r\n } else {\r\n frappe.msgprint(__('Could not fetch coordinates for the selected address.'));\r\n }\r\n });\r\n }\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Pre-Built Routes",
"enabled": 1,
"modified": "2024-12-13 09:07:36.356251",
"module": null,
"name": "Test - Render color coded map",
"script": "frappe.ui.form.on('Pre-Built Routes', {\r\n refresh: function(frm) {\r\n // Dynamically load Google Maps API if not already loaded\r\n if (!window.google || !window.google.maps) {\r\n const apiKey = 'AIzaSyCd3ALZe6wjt3xnc7X_rRItfKAEJugfuZ4'; // Replace with your actual API key\r\n const script = document.createElement('script');\r\n script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}`;\r\n script.async = true;\r\n script.defer = true;\r\n script.onload = function () {\r\n console.log(\"Google Maps API loaded successfully.\");\r\n renderMap(frm); // Render map after API is loaded\r\n };\r\n script.onerror = function () {\r\n frappe.msgprint(__('Failed to load Google Maps API. Please check your API key.'));\r\n };\r\n document.head.appendChild(script);\r\n return;\r\n }\r\n\r\n // If API is already loaded, render the map immediately\r\n renderMap(frm);\r\n }\r\n});\r\n\r\nfunction renderMap(frm) {\r\n const addresses = frm.doc.assigned_addresses || [];\r\n if (!addresses.length) {\r\n frappe.msgprint(__('No addresses to display on the map.'));\r\n return;\r\n }\r\n\r\n // Prepare points for the map\r\n const points = addresses.map(row => ({\r\n lat: parseFloat(row.latitude || 0),\r\n lng: parseFloat(row.longitude || 0),\r\n status: row.status || 'No Response',\r\n title: row.address_name || 'Unknown Address'\r\n }));\r\n\r\n // Define marker colors based on status\r\n const statusColors = {\r\n 'Confirmed': 'green',\r\n 'Reschedule': 'yellow',\r\n 'Declined': 'red',\r\n 'No Response': 'gray'\r\n };\r\n\r\n const mapContainer = frm.fields_dict['route_map'].wrapper;\r\n mapContainer.innerHTML = '<div id=\"route-map\" style=\"height: 400px;\"></div>';\r\n const map = new google.maps.Map(document.getElementById('route-map'), {\r\n zoom: 10,\r\n mapTypeId: 'roadmap'\r\n });\r\n\r\n const bounds = new google.maps.LatLngBounds();\r\n\r\n // Add markers for each point\r\n points.forEach(point => {\r\n if (point.lat && point.lng) {\r\n const marker = new google.maps.Marker({\r\n position: { lat: point.lat, lng: point.lng },\r\n map: map,\r\n title: point.title,\r\n icon: {\r\n path: google.maps.SymbolPath.CIRCLE,\r\n scale: 8,\r\n fillColor: statusColors[point.status],\r\n fillOpacity: 1,\r\n strokeWeight: 0\r\n }\r\n });\r\n bounds.extend(marker.getPosition());\r\n }\r\n });\r\n\r\n if (!bounds.isEmpty()) {\r\n map.fitBounds(bounds);\r\n }\r\n}\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Locate Log",
"enabled": 1,
"modified": "2024-12-16 14:47:43.738700",
"module": null,
"name": "calculated start date",
"script": "frappe.ui.form.on('Your DocType', {\r\n date_located: function(frm) {\r\n if (frm.doc.date_located) {\r\n var input_date = frm.doc.date_located;\r\n var calculated_date = addBusinessDays(input_date, 2);\r\n frm.set_value('legal_start_date', calculated_date);\r\n }\r\n }\r\n});\r\n\r\n// Function to calculate date after a specified number of business days\r\nfunction addBusinessDays(startDate, numBusinessDays) {\r\n var date = new Date(startDate);\r\n var daysAdded = 0;\r\n \r\n while (daysAdded < numBusinessDays) {\r\n date.setDate(date.getDate() + 1);\r\n // Check if the new date is a weekend (Saturday = 6, Sunday = 0)\r\n if (date.getDay() !== 0 && date.getDay() !== 6) {\r\n daysAdded++;\r\n }\r\n }\r\n \r\n // Return the calculated date in the correct format for ERPNext\r\n return frappe.datetime.add_days(date, 0);\r\n}\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Locate Log",
"enabled": 1,
"modified": "2024-12-16 14:47:43.716572",
"module": null,
"name": "calculated start date-list",
"script": "frappe.ui.form.on('Your DocType', {\r\n date_located: function(frm) {\r\n if (frm.doc.date_located) {\r\n var input_date = frm.doc.date_located;\r\n var calculated_date = addBusinessDays(input_date, 2);\r\n frm.set_value('legal_start_date', calculated_date);\r\n }\r\n }\r\n});\r\n\r\n// Function to calculate date after a specified number of business days\r\nfunction addBusinessDays(startDate, numBusinessDays) {\r\n var date = new Date(startDate);\r\n var daysAdded = 0;\r\n \r\n while (daysAdded < numBusinessDays) {\r\n date.setDate(date.getDate() + 1);\r\n // Check if the new date is a weekend (Saturday = 6, Sunday = 0)\r\n if (date.getDay() !== 0 && date.getDay() !== 6) {\r\n daysAdded++;\r\n }\r\n }\r\n \r\n // Return the calculated date in the correct format for ERPNext\r\n return frappe.datetime.add_days(date, 0);\r\n}\r\n",
"view": "List"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Locate Log",
"enabled": 1,
"modified": "2024-12-16 14:47:18.813864",
"module": null,
"name": "expiration date",
"script": "frappe.ui.form.on('Your DocType', {\r\n // Trigger when the \"Marked as Located\" button is clicked\r\n refresh: function(frm) {\r\n frm.add_custom_button(__('Marked as Located'), function() {\r\n update_expiration_date(frm);\r\n });\r\n }\r\n});\r\n\r\n// Function to update the expiration_date based on legal_start_date and state\r\nfunction update_expiration_date(frm) {\r\n if (frm.doc.legal_start_date && frm.doc.state) {\r\n var legalStartDate = frm.doc.legal_start_date;\r\n var expirationDate = null;\r\n \r\n // Check the state and calculate expiration date accordingly\r\n if (frm.doc.state === \"ID\") {\r\n expirationDate = frappe.datetime.add_days(legalStartDate, 28); // 28 days for ID\r\n } else if (frm.doc.state === \"WA\") {\r\n expirationDate = frappe.datetime.add_days(legalStartDate, 45); // 45 days for WA\r\n }\r\n\r\n // Set the calculated expiration date if a valid state is selected\r\n if (expirationDate) {\r\n frm.set_value('expiration_date', expirationDate);\r\n frappe.msgprint(__('Expiration Date has been calculated and updated.'));\r\n } else {\r\n frappe.msgprint(__('Please make sure both legal start date and state are filled.'));\r\n }\r\n } else {\r\n frappe.msgprint(__('Please make sure both legal start date and state are filled.'));\r\n }\r\n}\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Locate Log",
"enabled": 1,
"modified": "2024-12-16 14:53:13.214793",
"module": null,
"name": "expiration date -list",
"script": "frappe.ui.form.on('Your DocType', {\r\n date_located: function(frm) {\r\n if (frm.doc.date_located) {\r\n var input_date = frm.doc.date_located;\r\n var calculated_date = addBusinessDays(input_date, 30);\r\n frm.set_value('expiration_date', calculated_date);\r\n }\r\n }\r\n});\r\n\r\n// Function to calculate date after a specified number of business days\r\nfunction addBusinessDays(startDate, numBusinessDays) {\r\n var date = new Date(startDate);\r\n var daysAdded = 0;\r\n \r\n while (daysAdded < numBusinessDays) {\r\n date.setDate(date.getDate() + 1);\r\n // Check if the new date is a weekend (Saturday = 6, Sunday = 0)\r\n if (date.getDay() !== 0 && date.getDay() !== 6) {\r\n daysAdded++;\r\n }\r\n }\r\n \r\n // Return the calculated date in the correct format for ERPNext\r\n return frappe.datetime.add_days(date, 0);\r\n}\r\n",
"view": "List"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Quotation",
"enabled": 0,
"modified": "2025-01-24 13:24:25.882893",
"module": null,
"name": "Auto populate address from customer - test",
"script": "frappe.ui.form.on('Quotation', {\r\n customer: function(frm) {\r\n // Trigger when the customer field is set or changed\r\n if (frm.doc.customer) {\r\n frappe.call({\r\n method: 'frappe.client.get_value',\r\n args: {\r\n doctype: 'Customer',\r\n filters: { name: frm.doc.customer },\r\n fieldname: 'address_html'\r\n },\r\n callback: function(response) {\r\n if (response.message && response.message.address_html) {\r\n // Set the customer_address field in the Quotation\r\n frm.set_value('customer_address', response.message.address_html);\r\n } else {\r\n frappe.msgprint(__('No address found for the selected customer.'));\r\n }\r\n }\r\n });\r\n }\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Address",
"enabled": 0,
"modified": "2025-01-24 14:05:52.607856",
"module": "Brotherton SOP",
"name": "address concatenate",
"script": "frappe.ui.form.on('<Address>', {\n validate: function(frm) {\n // Concatenate the fields\n frm.set_value('address_line1', \n (frm.doc.custom_street_number || '') + ', ' + \n (frm.doc.custom_directional || '') + ', ' + \n (frm.doc.custom_street_name || '') + ', ' + \n (frm.doc.custom_street_suffix || '')\n );\n }\n});",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Address",
"enabled": 1,
"modified": "2025-05-15 12:30:18.280633",
"module": null,
"name": "Address Validation",
"script": "const renameInProgress = new Set();\r\nfunction rename(frm) {\r\n const newName = frm.doc.address_line1;\r\n if (!renameInProgress.has(newName)) {\r\n renameInProgress.add(newName);\r\n return frappe.call({\r\n method: 'frappe.rename_doc',\r\n freeze: true,\r\n \t\tfreeze_message: \"Updating name...\",\r\n args: {\r\n doctype: 'Address',\r\n old: frm.doc.name,\r\n new: newName,\r\n merge: 0\r\n },\r\n \r\n // From https://github.com/frappe/frappe/blob/f708acb59e3cdc9ec1a91bcdfc0f36d6d012cbf5/frappe/public/js/frappe/model/model.js#L787\r\n callback: function (r, rt) {\r\n \t\t\tif (!r.exc) {\r\n \t\t\t\t$(document).trigger(\"rename\", [\r\n \t\t\t\t\t'Address',\r\n \t\t\t\t\tfrm.doc.name,\r\n \t\t\t\t\tr.message || newName,\r\n \t\t\t\t]);\r\n \t\t\t}\r\n \t\t\trenameInProgress.delete(newName);\r\n \t\t},\r\n })\r\n }\r\n}\r\n\r\nfrappe.ui.form.on('Address', {\r\n // Trigger validation and formatting on refresh\r\n refresh: function (frm) {\r\n // Trigger field validation when the form is refreshed\r\n frm.trigger('format_address_fields');\r\n \r\n // Rename to remove appended type, if needed\r\n if (!frm.is_new() && frm.doc.name !== frm.doc.address_line1 && frm.doc.address_type === \"Other\") {\r\n // Trial and error, seems like 1 second is needed to not cause issues\r\n setTimeout(() => rename(frm), 1000);\r\n }\r\n },\r\n\r\n // Watch for changes in address_line1 and address_line2 fields\r\n address_line1: function (frm) {\r\n frm.trigger('format_address_fields');\r\n if (frm.doc.address_line1 && frm.doc.address_title !== frm.doc.address_line1) {\r\n frm.set_value('address_title', frm.doc.address_line1);\r\n }\r\n },\r\n address_line2: function (frm) {\r\n frm.trigger('format_address_fields');\r\n },\r\n\r\n // Format and validate address fields\r\n format_address_fields: function (frm) {\r\n // Helper function to capitalize text and remove punctuation\r\n function format_text(field) {\r\n let value = frm.doc[field] || '';\r\n // Remove punctuation and capitalize the text\r\n let formatted_value = value\r\n .replace(/[.,!?;:']/g, '') // Remove punctuation\r\n .toUpperCase(); // Capitalize text\r\n return formatted_value;\r\n }\r\n\r\n // Format address_line1 and address_line2\r\n const formatted_line1 = format_text('address_line1');\r\n const formatted_line2 = format_text('address_line2');\r\n\r\n // Set the formatted values back to the form\r\n if (formatted_line1 !== frm.doc.address_line1) {\r\n frm.set_value('address_line1', formatted_line1);\r\n }\r\n if (formatted_line2 !== frm.doc.address_line2) {\r\n frm.set_value('address_line2', formatted_line2);\r\n }\r\n\r\n // Validate if punctuation exists (after formatting)\r\n if (/[.,!?;:']/.test(frm.doc.address_line1) || /[.,!?;:']/.test(frm.doc.address_line2)) {\r\n frappe.msgprint(__('Punctuation is not allowed in address fields.'));\r\n }\r\n },\r\n\r\n // Before saving the document, validate the fields\r\n validate: function (frm) {\r\n const invalidFields = [];\r\n\r\n // Check if punctuation still exists in address fields\r\n ['address_line1', 'address_line2'].forEach(field => {\r\n if (/[.,!?;:']/.test(frm.doc[field])) {\r\n invalidFields.push(field);\r\n }\r\n });\r\n\r\n // If invalid fields exist, stop the save process and alert the user\r\n if (invalidFields.length > 0) {\r\n frappe.msgprint(__('Punctuation is not allowed in address fields: ') + invalidFields.join(', '));\r\n frappe.validated = false; // Prevent saving\r\n }\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "QB Export",
"enabled": 1,
"modified": "2025-02-04 03:12:39.473331",
"module": null,
"name": "QB Export",
"script": "frappe.ui.form.on('QB Export', {\n\tonload(frm) {\n\t if (!frm.doc.start_date) frm.doc.start_date = new Date(new Date().getFullYear(), new Date().getMonth()-1)\n if (!frm.doc.end_date) frm.doc.end_date = new Date(new Date().getFullYear(), new Date().getMonth(), 0)\n }\n})",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Customer",
"enabled": 1,
"modified": "2025-02-05 05:28:54.588303",
"module": null,
"name": "Customer - Allow Multiple Addresses To Be Asssigned",
"script": "frappe.ui.form.on('Custom Customer Address Link', {\r\n address_name: function(frm, cdt, cdn) {\r\n let row = locals[cdt][cdn];\r\n \r\n if (row.address_name) {\r\n frappe.call({\r\n method: \"frappe.client.get\",\r\n args: {\r\n doctype: \"Address\",\r\n name: row.address_name\r\n },\r\n callback: function(r) {\r\n if (r.message) {\r\n frappe.model.set_value(cdt, cdn, \"address_line1\", r.message.address_line1);\r\n frappe.model.set_value(cdt, cdn, \"city\", r.message.city);\r\n frappe.model.set_value(cdt, cdn, \"state\", r.message.state);\r\n frappe.model.set_value(cdt, cdn, \"pincode\", r.message.pincode);\r\n frappe.model.set_value(cdt, cdn, \"address_type\", r.message.address_type);\r\n }\r\n }\r\n });\r\n }\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Sales Order",
"enabled": 1,
"modified": "2025-03-05 05:13:03.150519",
"module": null,
"name": "Carry Over Installation Address",
"script": "frappe.ui.form.on(\"Sales Order\", {\r\n onload: function(frm) {\r\n if (!frm.doc.custom_installation_address && frm.doc.quotation) {\r\n frappe.db.get_value(\"Quotation\", frm.doc.quotation, \"custom_installation_address\")\r\n .then(r => {\r\n if (r.message.custom_installation_address) {\r\n frm.set_value(\"custom_installation_address\", r.message.custom_installation_address);\r\n \r\n }\r\n });\r\n }\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Sales Invoice",
"enabled": 1,
"modified": "2025-03-05 05:13:42.768423",
"module": null,
"name": "Quotation to Sales Invoice Carry Over",
"script": "frappe.ui.form.on(\"Sales Invoice\", {\r\n onload: function(frm) {\r\n if (!frm.doc.custom_installation_address && frm.doc.quotation) {\r\n frappe.db.get_value(\"Quotation\", frm.doc.quotation, \"custom_installation_address\")\r\n .then(r => {\r\n if (r.message && r.message.custom_installation_address) {\r\n frm.set_value(\"custom_installation_address\", r.message.custom_installation_address);\r\n }\r\n });\r\n }\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Work Order",
"enabled": 1,
"modified": "2025-03-05 07:09:42.680637",
"module": null,
"name": "Sales Order to Work Order Carry Over",
"script": "frappe.ui.form.on(\"Work Order\", {\r\n onload: function(frm) {\r\n if (!frm.doc.custom_installation_address && frm.doc.sales_order) {\r\n frappe.db.get_value(\"Sales Order\", frm.doc.sales_order, \"custom_installation_address\")\r\n .then(r => {\r\n if (r.message && r.message.custom_installation_address) {\r\n frm.set_value(\"custom_installation_address\", r.message.custom_installation_address);\r\n }\r\n });\r\n }\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Delivery Note",
"enabled": 1,
"modified": "2025-03-05 05:15:06.408083",
"module": null,
"name": "Sales Order to Delivery Note Carry Over",
"script": "frappe.ui.form.on(\"Delivery Note\", {\r\n onload: function(frm) {\r\n if (!frm.doc.custom_installation_address && frm.doc.against_sales_order) {\r\n frappe.db.get_value(\"Sales Order\", frm.doc.against_sales_order, \"custom_installation_address\")\r\n .then(r => {\r\n if (r.message && r.message.custom_installation_address) {\r\n frm.set_value(\"custom_installation_address\", r.message.custom_installation_address);\r\n }\r\n });\r\n }\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Payment Entry",
"enabled": 1,
"modified": "2025-04-25 04:17:25.626671",
"module": null,
"name": "Sales Invoice to Payment Entry Carry Over",
"script": "frappe.ui.form.on(\"Payment Entry\", {\r\n onload: function(frm) {\r\n if (!frm.doc.custom_installation_address && frm.doc.reference_doctype && frm.doc.reference_name) {\r\n let source_doctype = frm.doc.reference_doctype;\r\n let source_name = frm.doc.reference_name;\r\n\r\n frappe.db.get_value(\"Sales Invoice\", frm.doc.sales_invoice, \"custom_installation_address\")\r\n .then(r => {\r\n if (r.message && r.message.custom_installation_address) {\r\n frm.set_value(\"custom_installation_address\", r.message.custom_installation_address);\r\n }\r\n });\r\n }\r\n }\r\n});\r\n\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Job Card",
"enabled": 1,
"modified": "2025-03-05 05:22:37.566028",
"module": null,
"name": "Work Order to Job Card Carry Over",
"script": "frappe.ui.form.on(\"Job Card\", {\r\n onload: function(frm) {\r\n if (!frm.doc.custom_installation_address && frm.doc.work_order) {\r\n frappe.db.get_value(\"Work Order\", frm.doc.work_order, \"custom_installation_address\")\r\n .then(r => {\r\n if (r.message && r.message.custom_installation_address) {\r\n frm.set_value(\"custom_jobsite\", r.message.custom_installation_address);\r\n }\r\n });\r\n }\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Project",
"enabled": 1,
"modified": "2025-03-05 05:19:29.828616",
"module": null,
"name": "Sales Order to Project Carry Over",
"script": "frappe.ui.form.on(\"Project\", {\r\n onload: function(frm) {\r\n if (!frm.doc.custom_installation_address && frm.doc.sales_order) {\r\n frappe.db.get_value(\"Sales Order\", frm.doc.sales_order, \"custom_installation_address\")\r\n .then(r => {\r\n if (r.message && r.message.custom_installation_address) {\r\n frm.set_value(\"custom_installation_address\", r.message.custom_installation_address);\r\n }\r\n });\r\n }\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Address",
"enabled": 1,
"modified": "2025-09-02 11:38:09.302073",
"module": null,
"name": "Buttons on Address Doctype",
"script": "frappe.ui.form.on(\"Address\", {\r\n refresh: function (frm) {\r\n if (!frm.is_new()) {\r\n frm.add_custom_button(__('Schedule On-Site Meeting'), function () {\r\n sessionStorage.setItem('on-site-meeting-address', frm.doc.name);\r\n frappe.set_route(\"List\", \"On-Site Meeting\", \"Calendar\", \"On-Site Meeting Calendar\");\r\n }, __(\"Create\"));\r\n \r\n // Add button to create a Sales Order\r\n frm.add_custom_button(__('Create Sales Order'), function () {\r\n frappe.new_doc('Sales Order', {\r\n custom_installation_address: frm.doc.name,\r\n customer: frm.doc.custom_customer_to_bill\r\n });\r\n }, __(\"Create\"));\r\n \r\n // Add button to create a Quotation\r\n frm.add_custom_button(__('Create Quotation'), function () {\r\n frappe.new_doc('Quotation', {\r\n custom_installation_address: frm.doc.name,\r\n quotation_to: 'Customer',\r\n party_name: frm.doc.custom_customer_to_bill\r\n });\r\n }, __(\"Create\"));\r\n // Add button to create a new Service Appointment\r\n frm.add_custom_button(__('Create Service Appointment'), function() {\r\n frappe.new_doc('Service Appointment', {\r\n //custom_location_of_meeting: frm.doc.name,\r\n contact: frm.doc.custom_customer_to_bill\r\n });\r\n }, __(\"Create\"));\r\n }\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "On-Site Meeting",
"enabled": 1,
"modified": "2025-04-17 12:19:42.335880",
"module": null,
"name": "On-Site Meeting - autofill address from session Enabled",
"script": "frappe.ui.form.on('On-Site Meeting', {\n\tonload(frm) {\n\t if (sessionStorage.getItem('on-site-meeting-address')) {\n\t frm.set_value('address', sessionStorage.getItem('on-site-meeting-address'));\n\t console.log('start_time', frm.doc.start_time)\n\t if (frm.doc.start_time) {\n\t frm.save();\n\t }\n\t }\n\t},\n\tvalidate(frm) {\n\t let end = new Date(frm.doc.start_time + \" GMT\");\n\t end.setHours(end.getHours() + 1)\n\t frm.set_value('end_time', end.toISOString().replace('T', ' ').split('.')[0]);\n\t},\n\tafter_save(frm) {\n\t console.log('on submit')\n\t if (frm.doc.address === sessionStorage.getItem('on-site-meeting-address')) {\n\t frappe.set_route('Form', 'Address', frm.doc.address);\n\t }\n\t sessionStorage.removeItem('on-site-meeting-address');\n\t},\n\tafter_cancel(frm) {\n\t sessionStorage.removeItem('on-site-meeting-address');\n\t},\n\tafter_discard(frm) {\n\t sessionStorage.removeItem('on-site-meeting-address');\n\t},\n})",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Quotation",
"enabled": 1,
"modified": "2025-05-08 13:56:46.345762",
"module": null,
"name": "Quick-send quote button (SNW)",
"script": "frappe.ui.form.on('Quotation', {\n\trefresh(frm) {\n\t if (frm.doc.status == \"Open\" && frm.doc.company == \"Sprinklers Northwest\") {\n \t\tfrm.add_custom_button(__('Send Email'), async function () {\n \t\t const party = frm.doc.quotation_to && frm.doc.party_name ? await frappe.db.get_doc(frm.doc.quotation_to, frm.doc.party_name) : null;\n \t\t const address = frm.doc.custom_installation_address ? await frappe.db.get_doc('Address', frm.doc.custom_installation_address) : null;\n \t\t \n \t\t let email = null;\n \t\t if (party && party.email_id) {\n \t\t email = party.email_id;\n \t\t } else if (party && party.email_ids && party.email_ids.length) {\n \t\t const primary = party.email_ids.find(email => email.is_primary);\n \t\t if (primary) email = primary.email_id;\n \t\t else email = party.email_ids[0].email_id;\n \t\t } else if (address && address.email_id) {\n \t\t email = address.email_id;\n \t\t }\n \t\t \n \t\t if (!email) {\n \t\t frappe.msgprint(\"No email on customer or address\");\n \t\t return;\n \t\t }\n \t\t \n frappe.confirm(`Send quote to ${frm.doc.party_name} (${email})?`,\n async () => {\n const { message: { subject, message } } = await frappe.call({\n method: \"frappe.email.doctype.email_template.email_template.get_email_template\",\n type: \"POST\",\n args: {\n template_name: 'Quote with Actions - SNW',\n doc: frm.doc\n }\n });\n \n await frappe.call({\n method: \"frappe.core.doctype.communication.email.make\",\n type: 'POST',\n args: {\n recipients: email, \n subject,\n content: message,\n doctype: 'Quotation',\n name: frm.doc.name,\n send_email: 1,\n send_me_a_copy: 0,\n print_format: 'SNW Quotations',\n email_template: 'Quote with Actions - SNW',\n read_receipt: 1,\n print_letterhead: 1\n }\n });\n \n frappe.msgprint(\"Email Queued\");\n },\n null\n );\n });\n\t }\n\t}\n})",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Payment Entry",
"enabled": 1,
"modified": "2025-05-02 11:03:34.086145",
"module": null,
"name": "Installation Address Carry Over from Sales Order to Payment Address",
"script": "frappe.ui.form.on(\"Payment Entry\", {\r\n onload: function(frm) {\r\n if (!frm.doc.custom_installation_address && frm.doc.against_sales_order) {\r\n frappe.db.get_value(\"Sales Order\", frm.doc.against_sales_order, \"custom_installation_address\")\r\n .then(r => {\r\n if (r.message.custom_installation_address) {\r\n frm.set_value(\"custom_installation_address\", r.message.custom_installation_address);\r\n }\r\n });\r\n }\r\n }\r\n});\r\n\r\n\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Payment Entry",
"enabled": 1,
"modified": "2025-05-08 13:20:53.255294",
"module": null,
"name": "Fetch billing customer from installation address",
"script": "frappe.ui.form.on('Payment Entry', {\n\tcustom_installation_address(frm) {\n\t if (frm.doc.custom_installation_address) {\n\t frappe.db.get_doc('Address', frm.doc.custom_installation_address).then(doc => {\n frm.set_value('party_type', 'Customer')\n frm.set_value('party', doc.custom_customer_to_bill)\n })\n\t }\n\t}\n})",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Pre-Built Routes",
"enabled": 1,
"modified": "2025-06-17 02:12:53.095227",
"module": null,
"name": "Filter Route Technicians by Role",
"script": "frappe.ui.form.on('Pre-Built Routes', {\r\n onload: function(frm) {\r\n\r\n // Filter Crew Leader: Only show employees with designation = \"Crew Lead\"\r\n frm.set_query('crew_leader', () => {\r\n return {\r\n filters: {\r\n designation: 'Crew Lead',\r\n status: 'Active'\r\n }\r\n };\r\n });\r\n\r\n // Filter Technicians: Only show active employees who are NOT crew leads\r\n frm.fields_dict['assigned_technicians'].grid.get_field('employee').get_query = function() {\r\n return {\r\n filters: [\r\n ['designation', '!=', 'Crew Lead'],\r\n ['status', '=', 'Active']\r\n ]\r\n };\r\n };\r\n\r\n },\r\n\r\n // Optional: Prevent assigning crew leader as a technician too\r\n validate: function(frm) {\r\n let crew_leader = frm.doc.crew_leader;\r\n let techs = frm.doc.assigned_technicians || [];\r\n\r\n let duplicate = techs.find(row => row.employee === crew_leader);\r\n if (duplicate) {\r\n frappe.msgprint(__('Crew Leader cannot be listed as a Technician.'));\r\n frappe.validated = false;\r\n }\r\n }\r\n});\r\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Pre-Built Routes",
"enabled": 1,
"modified": "2025-06-17 02:20:56.937981",
"module": null,
"name": "Auto-Fetch Relevant Sales Order",
"script": "// Trigger this logic whenever the address_name field is changed\nfrappe.ui.form.on('Assigned Address', {\n address_name: function(frm, cdt, cdn) {\n let row = locals[cdt][cdn];\n// Exit early if no address is selected\n if (!row.address_name) return;\n // Call the server to fetch all Sales Orders with this installation address\n frappe.call({\n method: \"frappe.client.get_list\",\n args: {\n doctype: \"Sales Order\",\n filters: {\n custom_installation_address: row.address_name\n },\n fields: [\"name\", \"customer\", \"transaction_date\", \"status\", \"grand_total\"]\n },\n callback: function(response) {\n const orders = response.message;\n // Case: No Sales Orders found\n if (!orders || orders.length === 0) {\n frappe.msgprint(\"No Sales Orders found for this address.\");\n frappe.model.set_value(cdt, cdn, \"linked_sales_order\", \"\");\n return;\n }\n // Case: Exactly one Sales Order found — auto-select it\n if (orders.length === 1) {\n frappe.model.set_value(cdt, cdn, \"linked_sales_order\", orders[0].name);\n return;\n }\n\n // Case: Multiple Sales Orders found — show a dialog to select one\n\n // Create a user-friendly list of options for the select field\n const options = orders.map(order => {\n return {\n label: `${order.name} | ${order.customer} | ${frappe.datetime.str_to_user(order.transaction_date)} | ${order.status} | $${order.grand_total}`,\n value: order.name\n };\n });\n // Define and show a custom dialog for selecting the appropriate Sales Order\n const dialog = new frappe.ui.Dialog({\n title: \"Select Sales Order for \" + row.address_name,\n fields: [\n {\n fieldname: 'selected_so',\n label: 'Sales Order',\n fieldtype: 'Select',\n options: options,\n reqd: 1\n }\n ],\n primary_action_label: 'Select',\n primary_action(values) {\n frappe.model.set_value(cdt, cdn, \"linked_sales_order\", values.selected_so);\n dialog.hide();\n }\n });\n\n dialog.show();\n }\n });\n }\n});\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Project",
"enabled": 1,
"modified": "2025-08-29 17:31:31.663751",
"module": null,
"name": "Project Warranty Countdown",
"script": "frappe.ui.form.on('Project', {\n\trefresh(frm) {\n\t\tif (frm.doc.status == \"Completed\" && frm.doc.project_template == \"SNW Install\") {\n\t\t let message;\n\t\t const days = Math.abs(frappe.datetime.get_day_diff(frm.doc.custom_warranty_expiration_date, frm.doc.custom_completion_date));\n\t\t let dayMessage = days == 1 ? \"day\" : \"days\";\n\t\t if (frappe.datetime.get_today() <= frm.doc.custom_warranty_expiration_date) {\n\t\t message = `Warranty is valid for ${days} more ${dayMessage}`;\n\t\t } else {\n\t\t message = `Warranty has expired ${days} ${dayMessage} ago.`;\n\t\t }\n\t\t frm.set_value(\"custom_warranty_information\", message);\n\t\t}\n\t}\n});\n",
"view": "Form"
},
{
"docstatus": 0,
"doctype": "Client Script",
"dt": "Address",
"enabled": 1,
"modified": "2025-05-15 09:21:20.813661",
"module": null,
"name": "Address Error Fix",
"script": "frappe.ui.form.on('Address', {\r\n validate: function (frm) {\r\n // Default value for the custom field (if it doesn't exist in Address DocType)\r\n if (!frm.doc.hasOwnProperty('custom_is_your_company_address')) {\r\n frm.doc.custom_is_your_company_address = false;\r\n }\r\n\r\n // Custom validation logic\r\n // if (frm.doc.custom_is_your_company_address && !frm.doc.some_other_field) {\r\n // frappe.throw(__('Please ensure that the required fields are filled out for Company Address.'));\r\n // }\r\n }\r\n});\r\n",
"view": "Form"
}
]

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -192,6 +192,9 @@ doc_events = {
"Task": {
"before_insert": "custom_ui.events.task.before_insert",
"after_insert": "custom_ui.events.task.after_insert"
},
"Bid Meeting Note Form": {
"after_insert": "custom_ui.events.general.attach_bid_note_form_to_project_template"
}
}
@ -205,79 +208,26 @@ fixtures = [
{
"dt": "DocType",
"filters": [
["name", "in", [
"Quotation Template",
"Quotation Template Item",
"Customer Company Link",
"Customer Address Link",
"Customer Contact Link",
# New link doctypes
"Customer Project Link",
"Customer Quotation Link",
"Customer Sales Order Link",
"Customer On-Site Meeting Link",
"Lead Address Link",
"Lead Contact Link",
"Lead Companies Link",
"Lead Quotation Link",
"Lead On-Site Meeting Link",
"Address Project Link",
"Address Quotation Link",
"Address On-Site Meeting Link",
"Address Sales Order Link",
"Address Contact Link",
"Address Company Link",
"Contact Address Link",
"Address Task Link",
"Customer Task Link"
]]
]
},
{
"dt": "Custom Field",
"filters": [
["dt", "=", "Quotation"],
["fieldname", "in", [
"custom_quotation_template",
"custom_project_template"
]]
]
},
{
"dt": "Custom Field",
"filters": [
["dt", "=", "Sales Order"],
["fieldname", "=", "custom_project_template"]
]
},
{
"dt": "Custom Field",
"filters": [
["dt", "=", "Lead"],
["fieldname", "=", "custom_customer_name"]
]
},
{
"dt": "Custom Field",
"filters": [
["dt", "=", "Project Template"],
["fieldname", "=", "company"]
]
},
{
"dt": "Property Setter",
"filters": [
["doc_type", "=", "Lead"],
["doc_type", "=", "Project"],
["doc_type", "=", "Address"]
]
}
["custom", "=", 1]
]
},
# These don't have reliable flags → export all
{"dt": "Custom Field"},
{"dt": "Property Setter"},
{"dt": "Client Script"},
{"dt": "Server Script"},
{"dt": "Report"},
{"dt": "Print Format"},
{"dt": "Dashboard"},
{"dt": "Workspace"},
]
# Scheduled Tasks
# ---------------

View file

@ -19,6 +19,8 @@ def after_install():
frappe.reload_doctype("On-Site Meeting")
update_onsite_meeting_fields()
update_address_fields()
check_and_create_holiday_list()
create_snw_install_task_types()
build_frontend()
def after_migrate():
@ -33,6 +35,7 @@ def after_migrate():
frappe.reload_doctype(doctype)
check_and_create_holiday_list()
create_snw_install_task_types()
# update_address_fields()
# build_frontend()
@ -1020,3 +1023,143 @@ def get_all_sundays(year):
d += timedelta(days=7)
return sundays
def create_snw_install_task_types():
task_types = [
{
"title": "Locate",
"description": "Utility locate request prior to installation start.",
"base_date": "Start",
"offset_days": 7,
"offset_direction": "Before",
"calculate_from": "Service Appointment",
"trigger": "Scheduled",
},
{
"title": "Permit",
"description": "Permits required prior to installation start.",
"base_date": "Start",
"offset_days": 7,
"offset_direction": "Before",
"calculate_from": "Service Appointment",
"trigger": "Scheduled",
},
{
"title": "1/2 Down Payment",
"description": "Collect half down payment on project creation.",
"calculate_from": "Project",
"trigger": "Created",
"no_due_date": 1,
},
{
"title": "Machine Staging",
"description": "Stage machinery one day before installation start.",
"base_date": "Start",
"offset_days": 1,
"offset_direction": "Before",
"calculate_from": "Service Appointment",
"trigger": "Scheduled",
},
{
"title": "Final Invoice",
"description": "Send final invoice within 5 days of job completion.",
"base_date": "Completion",
"offset_days": 5,
"offset_direction": "After",
"calculate_from": "Service Appointment",
"trigger": "Completed",
},
{
"title": "Backflow Test",
"description": "Backflow test after job completion if quoted.",
"base_date": "Completion",
"offset_days": 0,
"offset_direction": "After",
"calculate_from": "Service Appointment",
"trigger": "Completed",
},
{
"title": "Schedule Permit Inspection",
"description": "Schedule permit inspection 5 days after completion.",
"base_date": "Completion",
"offset_days": 5,
"offset_direction": "After",
"calculate_from": "Service Appointment",
"trigger": "Completed",
},
{
"title": "15 Day Warranty Follow-Up",
"description": "15-day warranty follow-up after completion.",
"base_date": "Completion",
"offset_days": 15,
"offset_direction": "After",
"calculate_from": "Service Appointment",
"trigger": "Completed",
},
{
"title": "30 Day Warranty Check",
"description": "30-day warranty check after completion.",
"base_date": "Completion",
"offset_days": 30,
"offset_direction": "After",
"calculate_from": "Service Appointment",
"trigger": "Completed",
},
{
"title": "30 Day Payment Reminder",
"description": "Payment reminder sent 30 days after completion.",
"base_date": "Completion",
"offset_days": 30,
"offset_direction": "After",
"calculate_from": "Service Appointment",
"trigger": "Completed",
},
{
"title": "60 Day Late Payment Notice",
"description": "Late payment notification at 60 days post completion.",
"base_date": "Completion",
"offset_days": 60,
"offset_direction": "After",
"calculate_from": "Service Appointment",
"trigger": "Completed",
},
{
"title": "80 Day Lien Notice",
"description": "Lien notice if payment is still late after 80 days.",
"base_date": "Completion",
"offset_days": 80,
"offset_direction": "After",
"calculate_from": "Service Appointment",
"trigger": "Completed",
},
{
"title": "365 Day Warranty Call / Walk",
"description": "One-year warranty call or walk-through.",
"base_date": "Completion",
"offset_days": 365,
"offset_direction": "After",
"calculate_from": "Service Appointment",
"trigger": "Completed",
},
]
for task_type in task_types:
# Idempotency check
if frappe.db.exists("Task Type", task_type["title"]):
continue
doc = frappe.get_doc({
"doctype": "Task Type",
"title": task_type["title"],
"description": task_type["description"],
"base_date": task_type.get("base_date"),
"offset_days": task_type.get("offset_days", 0),
"offset_direction": task_type.get("offset_direction"),
"calculate_from": task_type.get("calculate_from", "Service Appointment"),
"trigger": task_type["trigger"],
"no_due_date": task_type.get("no_due_date", 0),
})
doc.insert(ignore_permissions=True)
frappe.db.commit()

View file

@ -3,4 +3,6 @@ from .contact_service import ContactService
from .db_service import DbService
from .client_service import ClientService
from .estimate_service import EstimateService
from .onsite_meeting_service import OnSiteMeetingService
from .onsite_meeting_service import OnSiteMeetingService
from .task_service import TaskService
from .service_appointment_service import ServiceAppointmentService

View file

@ -0,0 +1,53 @@
import frappe
from custom_ui.services import ContactService, AddressService, ClientService, DbService
class ServiceAppointmentService:
@staticmethod
def create(data):
"""Create a new Service Appointment document."""
print("DEBUG: Creating Service Appointment with data:", data)
service_appointment_doc = frappe.get_doc({
"doctype": "Service Appointment",
**data
})
service_appointment_doc.insert()
print("DEBUG: Created Service Appointment with name:", service_appointment_doc.name)
return service_appointment_doc
@staticmethod
def get_full_dict(service_appointment_name: str) -> dict:
"""Retrieve a Service Appointment document as a full dictionary."""
print(f"DEBUG: Retrieving Service Appointment document with name: {service_appointment_name}")
service_appointment = frappe.get_doc("Service Appointment", 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()
return service_appointment
@staticmethod
def update_scheduled_dates(service_appointment_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 Appointment", service_appointment_name)
service_appointment.expected_start_date = start_date
service_appointment.expected_end_date = end_date
if start_time:
service_appointment.expected_start_time = start_time
if end_time:
service_appointment.expected_end_time = end_time
service_appointment.save()
print(f"DEBUG: Updated scheduled dates for Service Appointment {service_appointment_name}")
return service_appointment
@staticmethod
def update_field(service_appointment_name: str, updates: list[tuple[str, any]]):
"""Update specific fields of a Service Appointment."""
print(f"DEBUG: Updating fields for Service Appointment {service_appointment_name} with updates: {updates}")
service_appointment = DbService.get_or_throw("Service Appointment", service_appointment_name)
for field, value in updates:
setattr(service_appointment, field, value)
service_appointment.save()
print(f"DEBUG: Updated fields for Service Appointment {service_appointment_name}")
return service_appointment

View file

@ -0,0 +1,143 @@
import frappe
from frappe.utils.safe_exec import safe_eval
from datetime import timedelta, datetime, date
class TaskService:
@staticmethod
def calculate_and_set_due_dates(task_names: list[str], event: str):
"""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)
@staticmethod
def get_tasks_by_project(project_name: str):
"""Retrieve all tasks associated with a given project."""
task_names = frappe.get_all("Task", filters={"project": project_name}, pluck="name")
tasks = [frappe.get_doc("Task", task_name) for task_name in task_names]
return tasks
@staticmethod
def check_and_update_task_due_date(task_name: str, event: str):
"""Determine the triggering configuration for a given task."""
task_type_doc = TaskService.get_task_type_doc(task_name)
if task_type_doc.trigger != event:
print(f"DEBUG: Task {task_name} trigger {task_type_doc.trigger} does not match event {event}, skipping calculation.")
return
if task_type_doc.logic_key:
print(f"DEBUG: Task {task_name} has a logic key set, skipping calculations and running logic.")
safe_eval(task_type_doc.logic_key, {"task_name": task_name, "task_type_doc": task_type_doc})
if task_type_doc.no_due_date:
print(f"DEBUG: Task {task_name} is marked as no due date, skipping calculation.")
return
calculated_from = task_type_doc.calculated_from
trigger = task_type_doc.trigger
print(f"DEBUG: Calculating triggering data for Task {task_name} from {calculated_from} on trigger {trigger}")
triggering_doc_dict = TaskService.get_triggering_doc_dict(task_name=task_name, task_type_doc=task_type_doc)
calculated_due_date, calculated_start_date = TaskService.calculate_dates(
task_name=task_name,
triggering_doc_dict=triggering_doc_dict,
task_type_doc=task_type_doc
)
update_required = TaskService.determine_update_required(
task_name=task_name,
calculated_due_date=calculated_due_date,
calculated_start_date=calculated_start_date
)
if update_required:
TaskService.update_task_dates(
task_name=task_name,
calculated_due_date=calculated_due_date,
calculated_start_date=calculated_start_date
)
@staticmethod
def get_task_type_doc(task_name: str):
task_type_name = frappe.get_value("Task", task_name, "task_type")
return frappe.get_doc("Task Type", task_type_name)
@staticmethod
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)
if offset_direction == "Before":
offset_days = -offset_days
base_date_field_value = triggering_doc_dict.get(base_date_field)
if isinstance(base_date_field_value, datetime):
base_date_field_value = base_date_field_value.date()
calculated_due_date = base_date_field_value + timedelta(days=offset_days)
calculated_start_date = None
if task_type_doc.days > 1:
calculated_start_date = calculated_due_date - timedelta(days=task_type_doc.days)
print(f"DEBUG: Calculated dates for Task {task_name} - Due Date: {calculated_due_date}, Start Date: {calculated_start_date}")
return calculated_due_date, calculated_start_date
@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")
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
else:
print(f"DEBUG: No update required for Task {task_name}. Dates are up to date.")
return False
@staticmethod
def get_triggering_doc_dict(task_name: str, task_type_doc) -> dict | None:
project_name = frappe.get_value("Task", task_name, "project")
dict = None
if task_type_doc.calculated_from == "Project":
dict = frappe.get_doc("Project", project_name).to_dict()
if task_type_doc.calculated_from == "Service Appointment":
service_name = frappe.get_value("Project", project_name, "service_appointment")
dict = frappe.get_doc("Service Appointment", service_name).to_dict()
if task_type_doc.calculated_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()
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.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:
"""Map a base date configuration to a corresponding field name."""
base_date_field_map = {
"Start": "expected_start_date",
"End": "expected_end_date",
"Creation": "creation",
"Completion": "actual_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:
return "Scheduled"
elif prev_doc.status != triggering_doc.status and triggering_doc.status == "Completed":
return "Completed"
else:
return None