diff --git a/custom_ui/api/db/estimates.py b/custom_ui/api/db/estimates.py index 501b219..86c1d2e 100644 --- a/custom_ui/api/db/estimates.py +++ b/custom_ui/api/db/estimates.py @@ -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) diff --git a/custom_ui/api/db/jobs.py b/custom_ui/api/db/jobs.py index affeb74..42a717d 100644 --- a/custom_ui/api/db/jobs.py +++ b/custom_ui/api/db/jobs.py @@ -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) diff --git a/custom_ui/api/db/service_appointments.py b/custom_ui/api/db/service_appointments.py index 2ac4b63..d02e707 100644 --- a/custom_ui/api/db/service_appointments.py +++ b/custom_ui/api/db/service_appointments.py @@ -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) \ No newline at end of file diff --git a/custom_ui/api/public/payments.py b/custom_ui/api/public/payments.py index a9249da..19f796e 100644 --- a/custom_ui/api/public/payments.py +++ b/custom_ui/api/public/payments.py @@ -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(): diff --git a/custom_ui/db_utils.py b/custom_ui/db_utils.py index e9be1be..e0b9561 100644 --- a/custom_ui/db_utils.py +++ b/custom_ui/db_utils.py @@ -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" } diff --git a/custom_ui/events/jobs.py b/custom_ui/events/jobs.py index b48197b..b083756 100644 --- a/custom_ui/events/jobs.py +++ b/custom_ui/events/jobs.py @@ -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() diff --git a/custom_ui/events/sales_invoice.py b/custom_ui/events/sales_invoice.py index 3330253..9e70a9b 100644 --- a/custom_ui/events/sales_invoice.py +++ b/custom_ui/events/sales_invoice.py @@ -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") \ No newline at end of file diff --git a/custom_ui/events/service_appointment.py b/custom_ui/events/service_appointment.py index d0f04de..23c5789 100644 --- a/custom_ui/events/service_appointment.py +++ b/custom_ui/events/service_appointment.py @@ -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()) \ No newline at end of file + 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") \ No newline at end of file diff --git a/custom_ui/events/task.py b/custom_ui/events/task.py index a5e24df..e69c64d 100644 --- a/custom_ui/events/task.py +++ b/custom_ui/events/task.py @@ -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()) - \ No newline at end of file + 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") \ No newline at end of file diff --git a/custom_ui/hooks.py b/custom_ui/hooks.py index 70ed613..55ec823 100644 --- a/custom_ui/hooks.py +++ b/custom_ui/hooks.py @@ -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 # ------- diff --git a/custom_ui/scheduled_tasks.py b/custom_ui/scheduled_tasks.py new file mode 100644 index 0000000..b19ec47 --- /dev/null +++ b/custom_ui/scheduled_tasks.py @@ -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() \ No newline at end of file diff --git a/custom_ui/services/estimate_service.py b/custom_ui/services/estimate_service.py index ea05a8a..8cc96a9 100644 --- a/custom_ui/services/estimate_service.py +++ b/custom_ui/services/estimate_service.py @@ -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.""" diff --git a/custom_ui/services/project_service.py b/custom_ui/services/project_service.py index b7a23da..af97126 100644 --- a/custom_ui/services/project_service.py +++ b/custom_ui/services/project_service.py @@ -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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/custom_ui/services/sales_order_service.py b/custom_ui/services/sales_order_service.py index ff3c2dd..39c863a 100644 --- a/custom_ui/services/sales_order_service.py +++ b/custom_ui/services/sales_order_service.py @@ -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 diff --git a/custom_ui/services/service_appointment_service.py b/custom_ui/services/service_appointment_service.py index a4d45d0..6e911c4 100644 --- a/custom_ui/services/service_appointment_service.py +++ b/custom_ui/services/service_appointment_service.py @@ -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 \ No newline at end of file diff --git a/custom_ui/services/task_service.py b/custom_ui/services/task_service.py index f03028b..f1d45e9 100644 --- a/custom_ui/services/task_service.py +++ b/custom_ui/services/task_service.py @@ -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() \ No newline at end of file diff --git a/custom_ui/templates/emails/payment-confirmation.html b/custom_ui/templates/emails/payment-confirmation.html new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/api.js b/frontend/src/api.js index 7bbbb0f..0be2552 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -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 // ============================================================================ diff --git a/frontend/src/components/calendar/CalendarNavigation.vue b/frontend/src/components/calendar/CalendarNavigation.vue index a415e8a..70ea977 100644 --- a/frontend/src/components/calendar/CalendarNavigation.vue +++ b/frontend/src/components/calendar/CalendarNavigation.vue @@ -1,6 +1,6 @@ diff --git a/frontend/src/components/pages/Jobs.vue b/frontend/src/components/pages/Jobs.vue index 32221b1..bc6513a 100644 --- a/frontend/src/components/pages/Jobs.vue +++ b/frontend/src/components/pages/Jobs.vue @@ -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);