Skip to content

Commit 90b7aba

Browse files
authored
Merge pull request #2140 from frappe/version-15-hotfix
chore: release v15
2 parents a75dc65 + e2f8d8a commit 90b7aba

File tree

86 files changed

+5686
-1330
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+5686
-1330
lines changed

.editorconfig

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ trim_trailing_whitespace = true
99
charset = utf-8
1010

1111
# js indentation settings
12-
[{*.js,*.vue,*.css,*.scss,*.html}]
12+
[{*.js,*.ts,*.vue,*.css,*.scss,*.html}]
1313
indent_style = tab
1414
indent_size = 4
15-
max_line_length = 99
15+
max_line_length = 99

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,5 @@ node_modules
1818
hrms/docs/current
1919
hrms/public/frontend
2020
hrms/www/hrms.html
21+
hrms/public/roster
22+
hrms/www/roster.html

.pre-commit-config.yaml

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ repos:
2020
rev: v3.1.0
2121
hooks:
2222
- id: prettier
23-
types_or: [javascript, vue, css, scss]
24-
# Ignore fronetend folder and any files that might contain jinja / bundles
23+
types_or: [javascript, ts, vue, css, scss]
24+
# Ignore frontend folder and any files that might contain jinja / bundles
2525
exclude: |
2626
(?x)^(
2727
frontend/.*|
@@ -46,4 +46,4 @@ repos:
4646
ci:
4747
autoupdate_schedule: weekly
4848
skip: []
49-
submodules: false
49+
submodules: false

frappe-ui

Submodule frappe-ui updated 53 files

frontend/package.json

+6-6
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,19 @@
1414
"@ionic/vue": "^7.4.3",
1515
"@ionic/vue-router": "^7.4.3",
1616
"@vitejs/plugin-vue": "^4.4.0",
17+
"autoprefixer": "^10.4.2",
1718
"dayjs": "^1.11.7",
1819
"feather-icons": "^4.28.0",
19-
"frappe-ui": "^0.1.59",
20-
"vue": "^3.2.25",
21-
"vue-router": "^4.0.12",
22-
"autoprefixer": "^10.4.2",
20+
"firebase": "^10.8.0",
21+
"frappe-ui": "^0.1.67",
2322
"postcss": "^8.4.5",
2423
"tailwindcss": "^3.0.15",
2524
"vite": "^5.1.4",
2625
"vite-plugin-pwa": "^0.19.0",
27-
"workbox-precaching": "^7.0.0",
26+
"vue": "^3.2.25",
27+
"vue-router": "^4.0.12",
2828
"workbox-core": "^7.0.0",
29-
"firebase": "^10.8.0"
29+
"workbox-precaching": "^7.0.0"
3030
},
3131
"devDependencies": {
3232
"eslint": "^8.39.0",

hrms/api/roster.py

+251
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
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

hrms/hooks.py

+2
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979

8080
website_route_rules = [
8181
{"from_route": "/hrms/<path:app_path>", "to_route": "hrms"},
82+
{"from_route": "/hr/<path:app_path>", "to_route": "roster"},
8283
]
8384
# Jinja
8485
# ----------
@@ -220,6 +221,7 @@
220221
],
221222
"hourly_long": [
222223
"hrms.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
224+
"hrms.hr.doctype.shift_assignment_schedule.shift_assignment_schedule.process_auto_shift_creation",
223225
],
224226
"daily": [
225227
"hrms.controllers.employee_reminders.send_birthday_reminders",

hrms/hr/doctype/shift_assignment/shift_assignment.json

+9-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"employee_name",
1212
"shift_type",
1313
"status",
14+
"schedule",
1415
"column_break_3",
1516
"company",
1617
"start_date",
@@ -101,11 +102,18 @@
101102
"fieldtype": "Select",
102103
"label": "Status",
103104
"options": "Active\nInactive"
105+
},
106+
{
107+
"fieldname": "schedule",
108+
"fieldtype": "Link",
109+
"label": "Schedule",
110+
"options": "Shift Assignment Schedule",
111+
"read_only": 1
104112
}
105113
],
106114
"is_submittable": 1,
107115
"links": [],
108-
"modified": "2024-04-04 17:13:13.137431",
116+
"modified": "2024-05-31 16:41:32.869130",
109117
"modified_by": "Administrator",
110118
"module": "HR",
111119
"name": "Shift Assignment",

0 commit comments

Comments
 (0)