|
| 1 | +import frappe |
| 2 | +from frappe import _ |
| 3 | +from frappe.utils import add_days, date_diff |
| 4 | + |
| 5 | +from erpnext.setup.doctype.employee.employee import get_holiday_list_for_employee |
| 6 | + |
| 7 | +from hrms.hr.doctype.shift_assignment.shift_assignment import ShiftAssignment |
| 8 | +from hrms.hr.doctype.shift_assignment_tool.shift_assignment_tool import create_shift_assignment |
| 9 | + |
| 10 | + |
| 11 | +@frappe.whitelist() |
| 12 | +def get_values(doctype: str, name: str, fields: list) -> dict[str, str]: |
| 13 | + return frappe.db.get_value(doctype, name, fields, as_dict=True) |
| 14 | + |
| 15 | + |
| 16 | +@frappe.whitelist() |
| 17 | +def get_events( |
| 18 | + month_start: str, month_end: str, employee_filters: dict[str, str], shift_filters: dict[str, str] |
| 19 | +) -> dict[str, list[dict]]: |
| 20 | + holidays = get_holidays(month_start, month_end, employee_filters) |
| 21 | + leaves = get_leaves(month_start, month_end, employee_filters) |
| 22 | + shifts = get_shifts(month_start, month_end, employee_filters, shift_filters) |
| 23 | + |
| 24 | + events = {} |
| 25 | + for event in [holidays, leaves, shifts]: |
| 26 | + for key, value in event.items(): |
| 27 | + if key in events: |
| 28 | + events[key].extend(value) |
| 29 | + else: |
| 30 | + events[key] = value |
| 31 | + return events |
| 32 | + |
| 33 | + |
| 34 | +@frappe.whitelist() |
| 35 | +def create_shift_assignment_schedule( |
| 36 | + employee: str, |
| 37 | + company: str, |
| 38 | + shift_type: str, |
| 39 | + status: str, |
| 40 | + start_date: str, |
| 41 | + end_date: str | None, |
| 42 | + repeat_on_days: list[str], |
| 43 | + frequency: str, |
| 44 | +) -> None: |
| 45 | + schedule = frappe.get_doc( |
| 46 | + { |
| 47 | + "doctype": "Shift Assignment Schedule", |
| 48 | + "frequency": frequency, |
| 49 | + "repeat_on_days": [{"day": day} for day in repeat_on_days], |
| 50 | + "enabled": 0 if end_date else 1, |
| 51 | + "employee": employee, |
| 52 | + "company": company, |
| 53 | + "shift_type": shift_type, |
| 54 | + "shift_status": status, |
| 55 | + } |
| 56 | + ).insert() |
| 57 | + |
| 58 | + if not end_date or date_diff(end_date, start_date) <= 90: |
| 59 | + return schedule.create_shifts(start_date, end_date) |
| 60 | + |
| 61 | + frappe.enqueue(schedule.create_shifts, timeout=4500, start_date=start_date, end_date=end_date) |
| 62 | + |
| 63 | + |
| 64 | +@frappe.whitelist() |
| 65 | +def delete_shift_assignment_schedule(schedule: str) -> None: |
| 66 | + for shift_assignment in frappe.get_all("Shift Assignment", {"schedule": schedule}, pluck="name"): |
| 67 | + doc = frappe.get_doc("Shift Assignment", shift_assignment) |
| 68 | + if doc.docstatus == 1: |
| 69 | + doc.cancel() |
| 70 | + frappe.delete_doc("Shift Assignment", shift_assignment) |
| 71 | + frappe.delete_doc("Shift Assignment Schedule", schedule) |
| 72 | + |
| 73 | + |
| 74 | +@frappe.whitelist() |
| 75 | +def swap_shift( |
| 76 | + src_shift: str, src_date: str, tgt_employee: str, tgt_date: str, tgt_shift: str | None |
| 77 | +) -> None: |
| 78 | + if src_shift == tgt_shift: |
| 79 | + frappe.throw(_("Source and target shifts cannot be the same")) |
| 80 | + |
| 81 | + if tgt_shift: |
| 82 | + tgt_shift_doc = frappe.get_doc("Shift Assignment", tgt_shift) |
| 83 | + tgt_company = tgt_shift_doc.company |
| 84 | + break_shift(tgt_shift_doc, tgt_date) |
| 85 | + else: |
| 86 | + tgt_company = frappe.db.get_value("Employee", tgt_employee, "company") |
| 87 | + |
| 88 | + src_shift_doc = frappe.get_doc("Shift Assignment", src_shift) |
| 89 | + break_shift(src_shift_doc, src_date) |
| 90 | + insert_shift( |
| 91 | + tgt_employee, tgt_company, src_shift_doc.shift_type, tgt_date, tgt_date, src_shift_doc.status |
| 92 | + ) |
| 93 | + |
| 94 | + if tgt_shift: |
| 95 | + insert_shift( |
| 96 | + src_shift_doc.employee, |
| 97 | + src_shift_doc.company, |
| 98 | + tgt_shift_doc.shift_type, |
| 99 | + src_date, |
| 100 | + src_date, |
| 101 | + tgt_shift_doc.status, |
| 102 | + ) |
| 103 | + |
| 104 | + |
| 105 | +@frappe.whitelist() |
| 106 | +def break_shift(assignment: str | ShiftAssignment, date: str) -> None: |
| 107 | + if isinstance(assignment, str): |
| 108 | + assignment = frappe.get_doc("Shift Assignment", assignment) |
| 109 | + |
| 110 | + if assignment.end_date and date_diff(assignment.end_date, date) < 0: |
| 111 | + frappe.throw(_("Cannot break shift after end date")) |
| 112 | + if date_diff(assignment.start_date, date) > 0: |
| 113 | + frappe.throw(_("Cannot break shift before start date")) |
| 114 | + |
| 115 | + employee = assignment.employee |
| 116 | + company = assignment.company |
| 117 | + shift_type = assignment.shift_type |
| 118 | + status = assignment.status |
| 119 | + end_date = assignment.end_date |
| 120 | + |
| 121 | + if date_diff(date, assignment.start_date) == 0: |
| 122 | + assignment.cancel() |
| 123 | + assignment.delete() |
| 124 | + else: |
| 125 | + assignment.end_date = add_days(date, -1) |
| 126 | + assignment.save() |
| 127 | + |
| 128 | + if not end_date or date_diff(end_date, date) > 0: |
| 129 | + create_shift_assignment(employee, company, shift_type, add_days(date, 1), end_date, status) |
| 130 | + |
| 131 | + |
| 132 | +@frappe.whitelist() |
| 133 | +def insert_shift( |
| 134 | + employee: str, company: str, shift_type: str, start_date: str, end_date: str | None, status: str |
| 135 | +) -> None: |
| 136 | + filters = { |
| 137 | + "doctype": "Shift Assignment", |
| 138 | + "employee": employee, |
| 139 | + "company": company, |
| 140 | + "shift_type": shift_type, |
| 141 | + "status": status, |
| 142 | + } |
| 143 | + prev_shift = frappe.db.exists(dict({"end_date": add_days(start_date, -1)}, **filters)) |
| 144 | + next_shift = ( |
| 145 | + frappe.db.exists(dict({"start_date": add_days(end_date, 1)}, **filters)) if end_date else None |
| 146 | + ) |
| 147 | + |
| 148 | + if prev_shift: |
| 149 | + if next_shift: |
| 150 | + end_date = frappe.db.get_value("Shift Assignment", next_shift, "end_date") |
| 151 | + frappe.db.set_value("Shift Assignment", next_shift, "docstatus", 2) |
| 152 | + frappe.delete_doc("Shift Assignment", next_shift) |
| 153 | + frappe.db.set_value("Shift Assignment", prev_shift, "end_date", end_date or None) |
| 154 | + |
| 155 | + elif next_shift: |
| 156 | + frappe.db.set_value("Shift Assignment", next_shift, "start_date", start_date) |
| 157 | + |
| 158 | + else: |
| 159 | + create_shift_assignment(employee, company, shift_type, start_date, end_date, status) |
| 160 | + |
| 161 | + |
| 162 | +def get_holidays(month_start: str, month_end: str, employee_filters: dict[str, str]) -> dict[str, list[dict]]: |
| 163 | + holidays = {} |
| 164 | + |
| 165 | + for employee in frappe.get_list("Employee", filters=employee_filters, pluck="name"): |
| 166 | + if holiday_list := get_holiday_list_for_employee(employee, raise_exception=False): |
| 167 | + holidays[employee] = frappe.get_all( |
| 168 | + "Holiday", |
| 169 | + filters={"parent": holiday_list, "holiday_date": ["between", [month_start, month_end]]}, |
| 170 | + fields=["name as holiday", "holiday_date", "description", "weekly_off"], |
| 171 | + ) |
| 172 | + |
| 173 | + return holidays |
| 174 | + |
| 175 | + |
| 176 | +def get_leaves(month_start: str, month_end: str, employee_filters: dict[str, str]) -> dict[str, list[dict]]: |
| 177 | + LeaveApplication = frappe.qb.DocType("Leave Application") |
| 178 | + Employee = frappe.qb.DocType("Employee") |
| 179 | + |
| 180 | + query = ( |
| 181 | + frappe.qb.select( |
| 182 | + LeaveApplication.name.as_("leave"), |
| 183 | + LeaveApplication.employee, |
| 184 | + LeaveApplication.leave_type, |
| 185 | + LeaveApplication.from_date, |
| 186 | + LeaveApplication.to_date, |
| 187 | + ) |
| 188 | + .from_(LeaveApplication) |
| 189 | + .left_join(Employee) |
| 190 | + .on(LeaveApplication.employee == Employee.name) |
| 191 | + .where( |
| 192 | + (LeaveApplication.docstatus == 1) |
| 193 | + & (LeaveApplication.status == "Approved") |
| 194 | + & (LeaveApplication.from_date <= month_end) |
| 195 | + & (LeaveApplication.to_date >= month_start) |
| 196 | + ) |
| 197 | + ) |
| 198 | + |
| 199 | + for filter in employee_filters: |
| 200 | + query = query.where(Employee[filter] == employee_filters[filter]) |
| 201 | + |
| 202 | + return group_by_employee(query.run(as_dict=True)) |
| 203 | + |
| 204 | + |
| 205 | +def get_shifts( |
| 206 | + month_start: str, month_end: str, employee_filters: dict[str, str], shift_filters: dict[str, str] |
| 207 | +) -> dict[str, list[dict]]: |
| 208 | + ShiftAssignment = frappe.qb.DocType("Shift Assignment") |
| 209 | + ShiftType = frappe.qb.DocType("Shift Type") |
| 210 | + Employee = frappe.qb.DocType("Employee") |
| 211 | + |
| 212 | + query = ( |
| 213 | + frappe.qb.select( |
| 214 | + ShiftAssignment.name, |
| 215 | + ShiftAssignment.employee, |
| 216 | + ShiftAssignment.shift_type, |
| 217 | + ShiftAssignment.start_date, |
| 218 | + ShiftAssignment.end_date, |
| 219 | + ShiftAssignment.status, |
| 220 | + ShiftType.start_time, |
| 221 | + ShiftType.end_time, |
| 222 | + ShiftType.color, |
| 223 | + ) |
| 224 | + .from_(ShiftAssignment) |
| 225 | + .left_join(ShiftType) |
| 226 | + .on(ShiftAssignment.shift_type == ShiftType.name) |
| 227 | + .left_join(Employee) |
| 228 | + .on(ShiftAssignment.employee == Employee.name) |
| 229 | + .where( |
| 230 | + (ShiftAssignment.docstatus == 1) |
| 231 | + & (ShiftAssignment.start_date <= month_end) |
| 232 | + & ((ShiftAssignment.end_date >= month_start) | (ShiftAssignment.end_date.isnull())) |
| 233 | + ) |
| 234 | + ) |
| 235 | + |
| 236 | + for filter in employee_filters: |
| 237 | + query = query.where(Employee[filter] == employee_filters[filter]) |
| 238 | + |
| 239 | + for filter in shift_filters: |
| 240 | + query = query.where(ShiftAssignment[filter] == shift_filters[filter]) |
| 241 | + |
| 242 | + return group_by_employee(query.run(as_dict=True)) |
| 243 | + |
| 244 | + |
| 245 | +def group_by_employee(events: list[dict]) -> dict[str, list[dict]]: |
| 246 | + grouped_events = {} |
| 247 | + for event in events: |
| 248 | + grouped_events.setdefault(event["employee"], []).append( |
| 249 | + {k: v for k, v in event.items() if k != "employee"} |
| 250 | + ) |
| 251 | + return grouped_events |
0 commit comments