all flow
This commit is contained in:
parent
c81beb5290
commit
38514fef47
@ -3,7 +3,7 @@ from frappe.utils.pdf import get_pdf
|
||||
from custom_ui.api.db.general import get_doc_history
|
||||
from custom_ui.db_utils import DbUtils, process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response
|
||||
from custom_ui.api.db.clients import check_if_customer, convert_lead_to_customer
|
||||
from custom_ui.services import DbService, ClientService, AddressService, ContactService, EstimateService, ItemService
|
||||
from custom_ui.services import ItemService, DbService, ClientService, AddressService, ContactService, EstimateService, ItemService
|
||||
from frappe.email.doctype.email_template.email_template import get_email_template
|
||||
|
||||
# ===============================================================================
|
||||
@ -145,6 +145,7 @@ def get_estimate(estimate_name):
|
||||
est_dict["address_details"] = address_doc
|
||||
|
||||
est_dict["history"] = get_doc_history("Quotation", estimate_name)
|
||||
est_dict["items"] = [ItemService.get_full_dict(item.item_code) for item in estimate.items]
|
||||
|
||||
return build_success_response(est_dict)
|
||||
except Exception as e:
|
||||
@ -532,7 +533,9 @@ def upsert_estimate(data):
|
||||
# AddressService.append_link(data.get("address_name"), "quotations", "quotation", new_estimate.name)
|
||||
# ClientService.append_link(data.get("customer"), "quotations", "quotation", new_estimate.name)
|
||||
print("DEBUG: New estimate created with name:", new_estimate.name)
|
||||
return build_success_response(new_estimate.as_dict())
|
||||
dict = new_estimate.as_dict()
|
||||
dict["items"] = [ItemService.get_full_dict(item.item_code) for item in new_estimate.items]
|
||||
return build_success_response(dict)
|
||||
except Exception as e:
|
||||
print(f"DEBUG: Error in upsert_estimate: {str(e)}")
|
||||
return build_error_response(str(e), 500)
|
||||
|
||||
@ -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, ServiceAppointmentService
|
||||
from custom_ui.db_utils import process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response, process_sorting
|
||||
from custom_ui.services import AddressService, ClientService, ServiceAppointmentService, ProjectService
|
||||
from frappe.utils import getdate
|
||||
|
||||
# ===============================================================================
|
||||
@ -119,13 +119,7 @@ def get_job(job_id=""):
|
||||
"""Get particular Job from DB"""
|
||||
print("DEBUG: Loading Job from database:", job_id)
|
||||
try:
|
||||
project = frappe.get_doc("Project", job_id)
|
||||
address_doc = AddressService.get_or_throw(project.job_address)
|
||||
project = project.as_dict()
|
||||
project["job_address"] = address_doc
|
||||
project["client"] = ClientService.get_client_or_throw(project.customer)
|
||||
task_names = frappe.get_all("Task", filters={"project": job_id})
|
||||
project["tasks"] = [frappe.get_doc("Task", task_name).as_dict() for task_name in task_names]
|
||||
project = ProjectService.get_full_project_details(job_id)
|
||||
return build_success_response(project)
|
||||
except Exception as e:
|
||||
return build_error_response(str(e), 500)
|
||||
@ -187,6 +181,8 @@ def get_job_task_list(job_id=""):
|
||||
def get_jobs_table_data(filters={}, sortings=[], page=1, page_size=10):
|
||||
"""Get paginated job table data with filtering and sorting support."""
|
||||
print("DEBUG: Raw job options received:", filters, sortings, page, page_size)
|
||||
filters = json.loads(filters) if isinstance(filters, str) else filters
|
||||
sortings = json.loads(sortings) if isinstance(sortings, str) else sortings
|
||||
|
||||
processed_filters, processed_sortings, is_or, page, page_size = process_query_conditions(filters, sortings, page, page_size)
|
||||
|
||||
|
||||
@ -2,6 +2,15 @@ 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_appointment(service_appointment_name):
|
||||
"""Get a single Service Appointment by name."""
|
||||
try:
|
||||
service_appointment = ServiceAppointmentService.get_full_dict(service_appointment_name)
|
||||
return build_success_response(service_appointment)
|
||||
except Exception as e:
|
||||
return build_error_response(str(e), 500)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_service_appointments(companies, filters={}):
|
||||
"""Get Service Appointments for given companies."""
|
||||
@ -65,5 +74,19 @@ def update_service_appointment_scheduled_dates(service_appointment_name: str, st
|
||||
end_time
|
||||
)
|
||||
return build_success_response(updated_service_appointment.as_dict())
|
||||
except Exception as e:
|
||||
return build_error_response(str(e), 500)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_service_appointment_status(service_appointment_name: str, new_status: str):
|
||||
"""Update status for a Service Appointment."""
|
||||
print(f"DEBUG: Updating status for Service Appointment {service_appointment_name} to new status: {new_status}")
|
||||
try:
|
||||
updated_service_appointment = ServiceAppointmentService.update_status(
|
||||
service_appointment_name,
|
||||
new_status
|
||||
)
|
||||
return build_success_response(updated_service_appointment.as_dict())
|
||||
except Exception as e:
|
||||
return build_error_response(str(e), 500)
|
||||
@ -26,6 +26,26 @@ def half_down_stripe_payment(sales_order):
|
||||
)
|
||||
frappe.local.response["type"] = "redirect"
|
||||
frappe.local.response["location"] = stripe_session.url
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def invoice_stripe_payment(sales_invoice):
|
||||
"""Public endpoint for initiating a full payment for a sales invoice."""
|
||||
if not DbService.exists("Sales Invoice", sales_invoice):
|
||||
frappe.throw("Sales Invoice does not exist.")
|
||||
si = DbService.get_or_throw("Sales Invoice", sales_invoice)
|
||||
if si.docstatus != 1:
|
||||
frappe.throw("Sales Invoice must be submitted to proceed with payment.")
|
||||
if si.outstanding_amount <= 0:
|
||||
frappe.throw("This invoice has already been paid.")
|
||||
stripe_session = StripeService.create_checkout_session(
|
||||
company=si.company,
|
||||
amount=si.outstanding_amount,
|
||||
service=si.project_template,
|
||||
order_num=si.name,
|
||||
for_advance_payment=False
|
||||
)
|
||||
frappe.local.response["type"] = "redirect"
|
||||
frappe.local.response["location"] = stripe_session.url
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def stripe_webhook():
|
||||
|
||||
@ -11,7 +11,7 @@ def map_field_name(frontend_field):
|
||||
"job_status": "custom_job_status",
|
||||
"installation_address": "custom_installation_address",
|
||||
"warranty_id": "name",
|
||||
"customer": "customer_name",
|
||||
"customer": "customer",
|
||||
"fromCompany": "from_company",
|
||||
"warranty_status": "warranty_amc_status"
|
||||
}
|
||||
|
||||
@ -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", current_triggering_dict=doc.as_dict())
|
||||
TaskService.fire_task_triggers(task_names, "Created", current_triggering_dict=doc.as_dict())
|
||||
|
||||
|
||||
|
||||
@ -74,7 +74,7 @@ def before_save(doc, method):
|
||||
doc.is_scheduled = 0
|
||||
event = TaskService.determine_event(doc)
|
||||
if event:
|
||||
TaskService.calculate_and_set_due_dates(
|
||||
TaskService.fire_task_triggers(
|
||||
[task.task for task in doc.tasks],
|
||||
event,
|
||||
current_triggering_dict=doc.as_dict()
|
||||
|
||||
@ -9,8 +9,14 @@ def on_submit(doc, method):
|
||||
print("DEBUG: Preparing to send invoice email for", doc.name)
|
||||
EmailService.send_invoice_email(doc.name)
|
||||
print("DEBUG: Invoice email sent successfully for", doc.name)
|
||||
frappe.set_value("Project", doc.project, "invoice_status", "Invoice Sent")
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to send invoice email: {str(e)}")
|
||||
# Don't raise the exception - we don't want to block the invoice submission
|
||||
frappe.log_error(f"Failed to send invoice email for {doc.name}: {str(e)}", "Invoice Email Error")
|
||||
|
||||
def after_insert(doc, method):
|
||||
print("DEBUG: After Insert Triggered for Sales Invoice:", doc.name)
|
||||
# Additional logic can be added here if needed after invoice creation
|
||||
frappe.set_value("Project", doc.project, "invoice_status", "Invoice Created")
|
||||
|
||||
@ -13,7 +13,7 @@ def on_update(doc, method):
|
||||
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", current_triggering_dict=doc.as_dict())
|
||||
TaskService.fire_task_triggers(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")
|
||||
@ -28,4 +28,6 @@ def before_save(doc, method):
|
||||
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())
|
||||
TaskService.fire_task_triggers(task_names=task_names, event=event, current_triggering_dict=doc.as_dict())
|
||||
if doc.status == "Completed" and frappe.get_value("Service Address 2", doc.name, "status") != "Completed":
|
||||
frappe.set_value("Project", doc.project, "invoice_status", "Ready to Invoice")
|
||||
@ -23,12 +23,33 @@ 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", current_triggering_dict=doc.as_dict())
|
||||
TaskService.fire_task_triggers(task_names, "Created", current_triggering_dict=doc.as_dict())
|
||||
|
||||
def before_save(doc, method):
|
||||
print("DEBUG: Before Save Triggered for Task:", doc.name)
|
||||
task_type_weight = frappe.get_value("Task Type", doc.type, "weight") or 0
|
||||
if doc.task_weight != task_type_weight:
|
||||
print(f"DEBUG: Updating Task weight from {doc.task_weight} to {task_type_weight}")
|
||||
doc.task_weight = task_type_weight
|
||||
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, current_triggering_dict=doc.as_dict())
|
||||
|
||||
TaskService.fire_task_triggers(task_names, event, current_triggering_dict=doc.as_dict())
|
||||
|
||||
def after_save(doc, method):
|
||||
print("DEBUG: After Save Triggered for Task:", doc.name)
|
||||
if doc.project and doc.status == "Completed":
|
||||
print("DEBUG: Task is completed, checking if project has calculated 100% Progress.")
|
||||
project_doc = frappe.get_doc("Project", doc.project)
|
||||
if project_doc.percent_complete == 100:
|
||||
project_update_required = False
|
||||
if project_doc.status == "Completed" and project_doc.customCompletionDate is None:
|
||||
print("DEBUG: Project is marked as Completed but customCompletionDate is not set, updating customCompletionDate.")
|
||||
project_doc.customCompletionDate = frappe.utils.nowdate()
|
||||
project_update_required = True
|
||||
if project_doc.invoice_status == "Not Ready":
|
||||
project_doc.invoice_status = "Ready to Invoice"
|
||||
project_update_required = True
|
||||
if project_update_required:
|
||||
project_doc.save(ignore_permissions=True)
|
||||
print("DEBUG: Updated Project document after Task completion")
|
||||
@ -246,13 +246,13 @@ fixtures = [
|
||||
# Scheduled Tasks
|
||||
# ---------------
|
||||
|
||||
# scheduler_events = {
|
||||
scheduler_events = {
|
||||
# "all": [
|
||||
# "custom_ui.tasks.all"
|
||||
# ],
|
||||
# "daily": [
|
||||
# "custom_ui.tasks.daily"
|
||||
# ],
|
||||
"daily": [
|
||||
"custom_ui.scheduled_tasks.daily"
|
||||
],
|
||||
# "hourly": [
|
||||
# "custom_ui.tasks.hourly"
|
||||
# ],
|
||||
@ -262,7 +262,7 @@ fixtures = [
|
||||
# "monthly": [
|
||||
# "custom_ui.tasks.monthly"
|
||||
# ],
|
||||
# }
|
||||
}
|
||||
|
||||
# Testing
|
||||
# -------
|
||||
|
||||
8
custom_ui/scheduled_tasks.py
Normal file
8
custom_ui/scheduled_tasks.py
Normal file
@ -0,0 +1,8 @@
|
||||
import frappe
|
||||
from custom_ui.services import TaskService
|
||||
|
||||
def daily_task():
|
||||
"""Scheduled task to run daily."""
|
||||
print("#################### Running Daily Task ####################")
|
||||
print("DEBUG: Checking Task due dates")
|
||||
TaskService.find_and_update_overdue_tasks()
|
||||
@ -22,6 +22,7 @@ class EstimateService:
|
||||
print("DEBUG: Quotation document not found.")
|
||||
return None
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_or_throw(estimate_name: str) -> frappe._dict:
|
||||
"""Retrieve a Quotation document by name or throw an error if not found."""
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import frappe
|
||||
from custom_ui.services import TaskService, AddressService
|
||||
|
||||
class ProjectService:
|
||||
|
||||
@ -9,4 +10,20 @@ class ProjectService:
|
||||
item_groups_str = frappe.db.get_value("Project Template", project_template, "item_groups") or ""
|
||||
item_groups = [item_group.strip() for item_group in item_groups_str.split(",") if item_group.strip()]
|
||||
print(f"DEBUG: Retrieved item groups: {item_groups}")
|
||||
return item_groups
|
||||
return item_groups
|
||||
|
||||
@staticmethod
|
||||
def get_full_project_details(project_name: str) -> dict:
|
||||
"""Retrieve comprehensive details for a given project, including linked sales order and invoice information."""
|
||||
print(f"DEBUG: Getting full project details for project: {project_name}")
|
||||
project = frappe.get_doc("Project", project_name).as_dict()
|
||||
project["tasks"] = [frappe.get_doc("Task", task["task"]).as_dict() for task in project["tasks"] if task.get("task")]
|
||||
for task in project["tasks"]:
|
||||
task["type"] = frappe.get_doc("Task Type", task["type"]).as_dict() if task.get("type") else None
|
||||
project["job_address"] = frappe.get_doc("Address", project["job_address"]).as_dict() if project["job_address"] else None
|
||||
project["service_appointment"] = frappe.get_doc("Service Address 2", project["service_appointment"]).as_dict() if project["service_appointment"] else None
|
||||
project["client"] = frappe.get_doc("Customer", project["customer"]).as_dict() if project["customer"] else None
|
||||
project["sales_order"] = frappe.get_doc("Sales Order", project["sales_order"]).as_dict() if project["sales_order"] else None
|
||||
project["billing_address"] = frappe.get_doc("Address", project["client"]["custom_billing_address"]).as_dict() if project["client"] and project["client"].get("custom_billing_address") else None
|
||||
project["invoice"] = frappe.get_doc("Sales Invoice", {"project": project["name"]}).as_dict() if frappe.db.exists("Sales Invoice", {"project": project["name"]}) else None
|
||||
return project
|
||||
@ -15,6 +15,11 @@ class SalesOrderService:
|
||||
sales_invoice.remarks = f"Auto-generated from Sales Order {sales_order_doc.name}"
|
||||
sales_invoice.job_address = sales_order_doc.custom_job_address
|
||||
sales_invoice.project_template = sales_order_doc.custom_project_template
|
||||
|
||||
sales_invoice.set_advances()
|
||||
sales_invoice.set_missing_values()
|
||||
sales_invoice.calculate_taxes_and_totals()
|
||||
|
||||
sales_invoice.insert()
|
||||
sales_invoice.submit()
|
||||
return sales_invoice.name
|
||||
|
||||
@ -51,4 +51,14 @@ class ServiceAppointmentService:
|
||||
setattr(service_appointment, field, value)
|
||||
service_appointment.save()
|
||||
print(f"DEBUG: Updated fields for Service Appointment {service_appointment_name}")
|
||||
return service_appointment
|
||||
|
||||
@staticmethod
|
||||
def update_status(service_appointment_name: str, new_status: str):
|
||||
"""Update the status of a Service Appointment."""
|
||||
print(f"DEBUG: Updating status for Service Appointment {service_appointment_name} to {new_status}")
|
||||
service_appointment = DbService.get_or_throw("Service Address 2", service_appointment_name)
|
||||
service_appointment.status = new_status
|
||||
service_appointment.save()
|
||||
print(f"DEBUG: Updated status for Service Appointment {service_appointment_name} to {new_status}")
|
||||
return service_appointment
|
||||
@ -6,10 +6,10 @@ class TaskService:
|
||||
|
||||
|
||||
@staticmethod
|
||||
def calculate_and_set_due_dates(task_names: list[str], event: str, current_triggering_dict=None):
|
||||
def fire_task_triggers(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, current_triggering_dict)
|
||||
TaskService.fire_task_trigger(task_name, event, current_triggering_dict)
|
||||
|
||||
|
||||
@staticmethod
|
||||
@ -20,49 +20,60 @@ class TaskService:
|
||||
return tasks
|
||||
|
||||
@staticmethod
|
||||
def check_and_update_task_due_date(task_name: str, event: str, current_triggering_dict=None):
|
||||
def fire_task_trigger(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 != current_triggering_dict.get("doctype") and current_triggering_dict:
|
||||
schedule_trigger = task_type_doc.triggering_doctype == current_triggering_dict.get("doctype") if current_triggering_dict else False
|
||||
completion_trigger = task_type_doc.custom_completion_trigger_doctype == current_triggering_dict.get("doctype") if current_triggering_dict else False
|
||||
match_schedule_event = task_type_doc.trigger == event
|
||||
match_completion_event = task_type_doc.custom_completion_trigger == event
|
||||
if not schedule_trigger and not completion_trigger:
|
||||
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:
|
||||
if not match_schedule_event and not match_completion_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
|
||||
calculate_from = task_type_doc.calculate_from
|
||||
trigger = task_type_doc.trigger
|
||||
print(f"DEBUG: Calculating triggering data for Task {task_name} from {calculate_from} on trigger {trigger}")
|
||||
|
||||
|
||||
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(
|
||||
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(
|
||||
if schedule_trigger:
|
||||
triggering_doc_dict = current_triggering_dict if current_triggering_dict else TaskService.get_triggering_doc_dict(task_name=task_name, doctype=task_type_doc.triggering_doctype, task_type_calculate_from=task_type_doc.task_type_calculate_from)
|
||||
calculate_from = task_type_doc.calculate_from
|
||||
trigger = task_type_doc.trigger
|
||||
print(f"DEBUG: Calculating triggering data for Task {task_name} from {calculate_from} on trigger {trigger}")
|
||||
|
||||
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_due_date_update_required(
|
||||
task_name=task_name,
|
||||
calculated_due_date=calculated_due_date,
|
||||
calculated_start_date=calculated_start_date
|
||||
)
|
||||
if update_required:
|
||||
TaskService.update_task(
|
||||
task_name=task_name,
|
||||
calculated_due_date=calculated_due_date,
|
||||
calculated_start_date=calculated_start_date
|
||||
)
|
||||
if completion_trigger:
|
||||
triggering_doc_dict = current_triggering_dict if current_triggering_dict else TaskService.get_triggering_doc_dict(task_name=task_name, doctype=task_type_doc.custom_completion_trigger_doctype)
|
||||
print(f"DEBUG: Running completion trigger logic for Task {task_name}")
|
||||
update_required = TaskService.determine_completion_update_required(
|
||||
task_name=task_name,
|
||||
task_type_doc=task_type_doc,
|
||||
)
|
||||
if update_required:
|
||||
TaskService.update_task(
|
||||
task_name=task_name,
|
||||
status="Completed"
|
||||
)
|
||||
print(f"DEBUG: Marked Task {task_name} as Completed due to completion trigger.")
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_task_type_doc(task_name: str):
|
||||
@ -94,7 +105,7 @@ class TaskService:
|
||||
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:
|
||||
def determine_due_date_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, "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:
|
||||
@ -104,32 +115,60 @@ class TaskService:
|
||||
print(f"DEBUG: No update required for Task {task_name}. Dates are up to date.")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def determine_completion_update_required(task_name: str, task_type_doc) -> bool:
|
||||
current_status = frappe.get_value("Task", task_name, "status")
|
||||
determination = False
|
||||
if current_status == "Completed":
|
||||
print(f"DEBUG: Task {task_name} is already marked as Completed, no update required.")
|
||||
return False
|
||||
else:
|
||||
triggering_doc_dict = TaskService.get_triggering_doc_dict(task_name=task_name, doctype=task_type_doc.custom_completion_trigger_doctype)
|
||||
check_field = TaskService.map_completion_check_field(task_type_doc.custom_completion_trigger)
|
||||
check_value = triggering_doc_dict.get(check_field)
|
||||
trigger = task_type_doc.custom_completion_trigger
|
||||
if trigger == "Completed" and check_value == "Completed" and current_status != "Completed":
|
||||
determination = True
|
||||
elif trigger == "Percentage Reached" and check_value >= task_type_doc.custom_target_percent and current_status != "Completed":
|
||||
determination = True
|
||||
elif trigger in ["Scheduled", "Started", "Created"] and check_value and current_status != "Completed":
|
||||
determination = True
|
||||
print(f"DEBUG: Completion trigger '{trigger}' met for Task {task_name}, check field {check_field} has value {check_value}.")
|
||||
return determination
|
||||
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_triggering_doc_dict(task_name: str, task_type_doc) -> dict | None:
|
||||
def get_triggering_doc_dict(task_name: str, doctype, task_type_calculate_from = None) -> 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":
|
||||
if doctype == "Project":
|
||||
dict = frappe.get_doc("Project", project_name).as_dict()
|
||||
if task_type_doc.calculate_from == "Service Address 2":
|
||||
if doctype == "Service Address 2":
|
||||
service_name = frappe.get_value("Project", project_name, "service_appointment")
|
||||
dict = frappe.get_doc("Service Address 2", service_name).as_dict()
|
||||
if task_type_doc.calculate_from == "Task":
|
||||
if doctype == "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:
|
||||
if task.task_type == task_type_calculate_from:
|
||||
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):
|
||||
def update_task(task_name: str, calculated_due_date: date | None = None, calculated_start_date: date | None = None, status: str | None = None):
|
||||
task_doc = frappe.get_doc("Task", task_name)
|
||||
task_doc.exp_end_date = calculated_due_date
|
||||
task_doc.exp_start_date = calculated_start_date
|
||||
if calculated_due_date is not None:
|
||||
task_doc.exp_end_date = calculated_due_date
|
||||
if calculated_start_date is not None:
|
||||
task_doc.exp_start_date = calculated_start_date
|
||||
if status is not None:
|
||||
task_doc.status = status
|
||||
if status == "Completed":
|
||||
task_doc.actual_end_date = datetime.now()
|
||||
task_doc.save(ignore_permissions=True)
|
||||
print(f"DEBUG: Updated Task {task_name} with new dates - Start: {calculated_start_date}, End: {calculated_due_date}")
|
||||
print(f"DEBUG: Updated Task {task_name} with new dates - Start: {calculated_start_date}, End: {calculated_due_date}, Status: {status}")
|
||||
|
||||
@staticmethod
|
||||
def map_base_date_to_field(base_date: str, triggering_doctype: str) -> str:
|
||||
@ -150,6 +189,17 @@ class TaskService:
|
||||
return task_date_field_map.get(base_date, "exp_end_date")
|
||||
return base_date_field_map.get(base_date, "expected_end_date")
|
||||
|
||||
@staticmethod
|
||||
def map_completion_check_field(completion_trigger: str) -> str:
|
||||
completion_check_field_map = {
|
||||
"Completed": "status",
|
||||
"Scheduled": "expected_end_date",
|
||||
"Created": "creation",
|
||||
"Started": "actual_start_date",
|
||||
"Percentage Reached": "progress"
|
||||
}
|
||||
return completion_check_field_map.get(completion_trigger, "status")
|
||||
|
||||
@staticmethod
|
||||
def determine_event(triggering_doc) -> str | None:
|
||||
print("DEBUG: Current Document:", triggering_doc.as_dict())
|
||||
@ -168,4 +218,16 @@ class TaskService:
|
||||
return "Completed"
|
||||
else:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def find_and_update_overdue_tasks():
|
||||
today = date.today()
|
||||
overdue_tasks = frappe.get_all("Task", filters={"exp_end_date": ("<", today), "status": ["not in", ["Completed", "Template", "Cancelled", "Overdue"]]}, pluck="name")
|
||||
print(f"DEBUG: Found {len(overdue_tasks)} overdue tasks.")
|
||||
for task_name in overdue_tasks:
|
||||
task_doc = frappe.get_doc("Task", task_name)
|
||||
task_doc.status = "Overdue"
|
||||
task_doc.save(ignore_permissions=True)
|
||||
print(f"DEBUG: Updated Task {task_name} to Overdue status.")
|
||||
frappe.db.commit()
|
||||
|
||||
@ -70,8 +70,10 @@ const FRAPPE_GET_EMPLOYEES_ORGANIZED_METHOD = "custom_ui.api.db.employees.get_em
|
||||
const FRAPPE_GET_WEEK_HOLIDAYS_METHOD = "custom_ui.api.db.general.get_week_holidays";
|
||||
const FRAPPE_GET_DOC_LIST_METHOD = "custom_ui.api.db.general.get_doc_list";
|
||||
// Service Appointment methods
|
||||
const FRAPPE_GET_SERVICE_APPOINTMENT_METHOD = "custom_ui.api.db.service_appointments.get_service_appointment";
|
||||
const FRAPPE_GET_SERVICE_APPOINTMENTS_METHOD = "custom_ui.api.db.service_appointments.get_service_appointments";
|
||||
const FRAPPE_UPDATE_SERVICE_APPOINTMENT_SCHEDULED_DATES_METHOD = "custom_ui.api.db.service_appointments.update_service_appointment_scheduled_dates";
|
||||
const FRAPPE_UPDATE_SERVICE_APPOINTMENT_STATUS_METHOD = "custom_ui.api.db.service_appointments.update_service_appointment_status";
|
||||
class Api {
|
||||
// ============================================================================
|
||||
// CORE REQUEST METHOPD
|
||||
@ -457,6 +459,10 @@ class Api {
|
||||
// SERVICE APPOINTMENT METHODS
|
||||
// ============================================================================
|
||||
|
||||
static async getServiceAppointment(serviceAppointmentName) {
|
||||
return await this.request(FRAPPE_GET_SERVICE_APPOINTMENT_METHOD, { serviceAppointmentName });
|
||||
}
|
||||
|
||||
static async getServiceAppointments(companies = [], filters = {}) {
|
||||
return await this.request(FRAPPE_GET_SERVICE_APPOINTMENTS_METHOD, { companies, filters });
|
||||
}
|
||||
@ -472,6 +478,10 @@ class Api {
|
||||
})
|
||||
}
|
||||
|
||||
static async setServiceAppointmentStatus(serviceAppointmentName, newStatus) {
|
||||
return await this.request(FRAPPE_UPDATE_SERVICE_APPOINTMENT_STATUS_METHOD, { serviceAppointmentName, newStatus });
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TASK METHODS
|
||||
// ============================================================================
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="calendar-navigation">
|
||||
<Tabs value="0" v-if="companyStore.currentCompany == 'Sprinklers Northwest'">
|
||||
<Tabs :value="defaultTab" v-if="companyStore.currentCompany == 'Sprinklers Northwest'">
|
||||
<TabList>
|
||||
<Tab value="0">Bids</Tab>
|
||||
<Tab value="1">Projects</Tab>
|
||||
@ -26,7 +26,7 @@
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<Tabs v-else value="0">
|
||||
<Tabs v-else :value="defaultTab">
|
||||
<TabList>
|
||||
<Tab value="0">Bids</Tab>
|
||||
<Tab value="1">Projects</Tab>
|
||||
@ -61,20 +61,36 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import Tab from 'primevue/tab';
|
||||
import Tabs from 'primevue/tabs';
|
||||
import TabList from 'primevue/tablist';
|
||||
import TabPanel from 'primevue/tabpanel';
|
||||
import TabPanels from 'primevue/tabpanels';
|
||||
import ScheduleBid from '../calendar/bids/ScheduleBid.vue';
|
||||
import JobsCalendar from '../calendar/jobs/JobsCalendar.vue';
|
||||
import SNWProjectCalendar from './jobs/SNWProjectCalendar.vue';
|
||||
import { useNotificationStore } from '../../stores/notifications-primevue';
|
||||
import { useCompanyStore } from '../../stores/company';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const notifications = useNotificationStore();
|
||||
const companyStore = useCompanyStore();
|
||||
const route = useRoute();
|
||||
|
||||
const defaultTab = ref('0');
|
||||
|
||||
const mappedTabs = {
|
||||
'bid': '0',
|
||||
'projects': '1',
|
||||
'service': '2',
|
||||
'warranties': companyStore.currentCompany == 'Sprinklers Northwest' ? '6' : '3'
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// Check for route query parameter to set default tab
|
||||
if (route.query.tab) {
|
||||
defaultTab.value = mappedTabs[route.query.tab] || '0';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -330,10 +330,14 @@ import { ref, onMounted, computed, watch } from "vue";
|
||||
import Api from "../../../api";
|
||||
import { useNotificationStore } from "../../../stores/notifications-primevue";
|
||||
import { useCompanyStore } from "../../../stores/company";
|
||||
import { useRoute } from "vue-router";
|
||||
import JobDetailsModal from "../../modals/JobDetailsModal.vue";
|
||||
|
||||
const notifications = useNotificationStore();
|
||||
const companyStore = useCompanyStore();
|
||||
const route = useRoute();
|
||||
|
||||
const serviceAptToFind = route.query.apt || null;
|
||||
|
||||
// Reactive data
|
||||
const scheduledServices = ref([]);
|
||||
@ -1302,6 +1306,40 @@ const stopResize = async () => {
|
||||
originalEndDate.value = null;
|
||||
};
|
||||
|
||||
const handleServiceAptToFind = async () => {
|
||||
if (!serviceAptToFind) return;
|
||||
|
||||
try {
|
||||
// Fetch the specific service appointment
|
||||
const appointment = await Api.getServiceAppointment(serviceAptToFind);
|
||||
|
||||
if (appointment && appointment.expectedStartDate) {
|
||||
// Navigate to the week containing this appointment
|
||||
weekStartDate.value = getWeekStart(parseLocalDate(appointment.expectedStartDate));
|
||||
|
||||
// Wait for data to load
|
||||
await fetchServiceAppointments();
|
||||
|
||||
// Find the appointment in the loaded data
|
||||
const foundAppointment = scheduledServices.value.find(s => s.name === serviceAptToFind) ||
|
||||
unscheduledServices.value.find(s => s.name === serviceAptToFind);
|
||||
|
||||
if (foundAppointment) {
|
||||
// Open the details modal
|
||||
selectedEvent.value = foundAppointment;
|
||||
eventDialog.value = true;
|
||||
} else {
|
||||
notifications.addWarning("Service appointment found but not visible in current filters");
|
||||
}
|
||||
} else {
|
||||
notifications.addWarning("Service appointment not scheduled yet");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error finding service appointment:", error);
|
||||
notifications.addError("Failed to find service appointment");
|
||||
}
|
||||
};
|
||||
|
||||
const fetchServiceAppointments = async (currentDate) => {
|
||||
try {
|
||||
// Calculate date range for the week
|
||||
@ -1397,6 +1435,9 @@ onMounted(async () => {
|
||||
// await fetchProjects();
|
||||
await fetchServiceAppointments();
|
||||
await fetchHolidays();
|
||||
|
||||
// Handle serviceAptToFind query parameter
|
||||
await handleServiceAptToFind();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@ -187,6 +187,17 @@
|
||||
<v-icon left>mdi-open-in-new</v-icon>
|
||||
View Job
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="job.status !== 'Completed'"
|
||||
color="success"
|
||||
variant="flat"
|
||||
@click="updateStatus('Completed')"
|
||||
:loading="isUpdatingStatus"
|
||||
:disabled="isUpdatingStatus"
|
||||
>
|
||||
<v-icon left>mdi-check-circle</v-icon>
|
||||
Mark as Completed
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="handleClose">Close</v-btn>
|
||||
</v-card-actions>
|
||||
@ -196,6 +207,10 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import Api from "../../api";
|
||||
import { useNotificationStore } from "../../stores/notifications-primevue";
|
||||
|
||||
const notifications = useNotificationStore();
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
@ -214,7 +229,10 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["update:modelValue", "close"]);
|
||||
const emit = defineEmits(["update:modelValue", "close", "statusUpdated"]);
|
||||
|
||||
// State
|
||||
const isUpdatingStatus = ref(false);
|
||||
|
||||
// Computed
|
||||
const showModal = computed({
|
||||
@ -304,6 +322,34 @@ const getPriorityColor = (priority) => {
|
||||
}
|
||||
};
|
||||
|
||||
const updateStatus = async (newStatus) => {
|
||||
if (!props.job?.name) return;
|
||||
|
||||
isUpdatingStatus.value = true;
|
||||
try {
|
||||
await Api.setServiceAppointmentStatus(props.job.name, newStatus);
|
||||
notifications.addSuccess(`Service appointment marked as ${newStatus}`);
|
||||
|
||||
// Update local job status
|
||||
if (props.job) {
|
||||
props.job.status = newStatus;
|
||||
}
|
||||
|
||||
// Emit event to parent to refresh data
|
||||
emit("statusUpdated", { name: props.job.name, status: newStatus });
|
||||
|
||||
// Close modal after short delay
|
||||
setTimeout(() => {
|
||||
handleClose();
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error("Error updating service appointment status:", error);
|
||||
notifications.addError("Failed to update service appointment status");
|
||||
} finally {
|
||||
isUpdatingStatus.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const viewJob = () => {
|
||||
if (props.job?.name) {
|
||||
window.location.href = `/job?name=${encodeURIComponent(props.job.name)}`;
|
||||
|
||||
@ -1163,6 +1163,8 @@ onMounted(async () => {
|
||||
qty: item.qty,
|
||||
rate: item.rate,
|
||||
standardRate: item.rate,
|
||||
bom: item.bom || null,
|
||||
uom: item.uom || item.stockUom || item.stock_uom || 'Nos',
|
||||
discountAmount: discountAmount === 0 ? null : discountAmount,
|
||||
discountPercentage: discountPercentage === 0 ? null : discountPercentage,
|
||||
discountType: discountPercentage > 0 ? 'percentage' : 'currency'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -136,9 +136,9 @@ const chartData = ref({
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ label: "Job ID", fieldName: "name", type: "text", sortable: true, filterable: true },
|
||||
{ label: "Address", fieldName: "jobAddress", type: "text", sortable: true },
|
||||
{ label: "Customer", fieldName: "customer", type: "text", sortable: true, filterable: true },
|
||||
{ label: "Address", fieldName: "jobAddress", type: "text", sortable: true },
|
||||
{ label: "Job ID", fieldName: "name", type: "text", sortable: true, filterable: true },
|
||||
{ label: "Overall Status", fieldName: "status", type: "status", sortable: true },
|
||||
{ label: "Invoice Status", fieldName: "invoiceStatus", type: "text", sortable: true },
|
||||
{ label: "Progress", fieldName: "percentComplete", type: "text", sortable: true }
|
||||
@ -275,7 +275,6 @@ const loadChartData = async () => {
|
||||
|
||||
// Load initial data
|
||||
onMounted(async () => {
|
||||
notifications.addWarning("Jobs page coming soon");
|
||||
// Initialize pagination and filters
|
||||
paginationStore.initializeTablePagination("jobs", { rows: 10 });
|
||||
filtersStore.initializeTableFilters("jobs", columns);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user