Skip to content

Commit 65120cc

Browse files
committed
[FIX] hr_holidays_ux: splits multi-month leaves in write and create.
1 parent 7e0ce7b commit 65120cc

1 file changed

Lines changed: 171 additions & 14 deletions

File tree

hr_holidays_ux/models/hr_leave.py

Lines changed: 171 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,25 @@ def get_last_day_of_month(self, date):
1515
return date.replace(day=last_day)
1616

1717
def split_days(self, rec):
18-
"""Split leave days across months."""
18+
"""
19+
Split leave days across months.
20+
"""
1921
new_records = []
2022
date_from = rec["request_date_from"]
2123
original_date_to = rec["request_date_to"]
2224

25+
# Ensure we're working with datetime objects for consistent comparison
26+
date_from = self._convert_date_to_datetime(date_from)
27+
original_date_to = self._convert_date_to_datetime(original_date_to)
28+
2329
while date_from <= original_date_to:
24-
# get last day of the month
25-
end_of_month = self.get_last_day_of_month(date_from)
30+
# Convert to date for get_last_day_of_month, then back to datetime
31+
date_from_date = self._convert_to_date(date_from)
32+
end_of_month_date = self.get_last_day_of_month(date_from_date)
33+
34+
# Convert back to datetime for consistent comparison
35+
end_of_month = self._convert_date_to_datetime(end_of_month_date)
36+
2637
if original_date_to <= end_of_month:
2738
# save the dates of the single record to be created
2839
new_records.append(
@@ -40,33 +51,37 @@ def split_days(self, rec):
4051
"request_date_to": end_of_month,
4152
}
4253
)
43-
date_from = end_of_month + timedelta(days=1)
54+
# Move to the first day of next month
55+
next_month_date = end_of_month_date + timedelta(days=1)
56+
date_from = self._convert_date_to_datetime(next_month_date)
4457

4558
return new_records
4659

4760
@api.model_create_multi
4861
def create(self, vals_list):
62+
"""
63+
Override create to automatically split leave records across months.
64+
65+
When creating leave records that span multiple months, this method
66+
automatically splits them into separate records for each month.
67+
"""
4968
new_vals_list = []
5069
for vals in vals_list:
5170
date_from = vals.get("request_date_from")
5271
date_to = vals.get("request_date_to")
5372

54-
if date_from and date_to:
55-
# convert to datetime objects if they are strings
56-
if isinstance(date_from, str):
57-
date_from = fields.Datetime.from_string(date_from)
58-
if isinstance(date_to, str):
59-
date_to = fields.Datetime.from_string(date_to)
60-
61-
# if the leave ends in a different month, call the split_days() method to get the dates for splitting the leaves
62-
if date_to > self.get_last_day_of_month(date_from):
73+
# If the leave spans multiple months, split it into separate records
74+
if date_from and date_to and self._should_split_leave(date_from, date_to):
75+
# Convert to datetime objects for split_days method
76+
date_from = self._convert_date_to_datetime(date_from)
77+
date_to = self._convert_date_to_datetime(date_to)
6378
split_records = self.split_days(
6479
{
6580
"request_date_from": date_from,
6681
"request_date_to": date_to,
6782
}
6883
)
69-
# for each record generated in split_days, copy the original vals, and update with the dates of the record rec, adding them to a new list
84+
# Create a separate record for each split period
7085
for rec in split_records:
7186
new_vals = vals.copy()
7287
new_vals.update(rec)
@@ -76,6 +91,148 @@ def create(self, vals_list):
7691

7792
return super().create(new_vals_list)
7893

94+
def _convert_date_to_datetime(self, date):
95+
"""Convert string or date object to datetime object if needed."""
96+
if isinstance(date, str):
97+
return fields.Datetime.from_string(date)
98+
elif hasattr(date, "year") and not hasattr(date, "hour"):
99+
# It's a date object, convert to datetime at start of day
100+
from datetime import datetime, time
101+
102+
return datetime.combine(date, time.min)
103+
return date
104+
105+
def _convert_to_date(self, date):
106+
"""Convert datetime or string to date object for comparison."""
107+
if isinstance(date, str):
108+
return fields.Date.from_string(date)
109+
elif hasattr(date, "date"):
110+
# datetime object
111+
return date.date()
112+
return date
113+
114+
def _should_split_leave(self, date_from, date_to):
115+
"""Check if leave period needs to be split across months."""
116+
# Convert to date objects for month comparison
117+
date_from = self._convert_to_date(date_from)
118+
date_to = self._convert_to_date(date_to)
119+
last_day_of_month = self.get_last_day_of_month(date_from)
120+
return date_to > last_day_of_month
121+
122+
def _check_overlapping_leaves(self, record, date_from, date_to):
123+
"""Check if there are existing leave records that overlap with the given dates."""
124+
domain = [
125+
("employee_id", "=", record.employee_id.id),
126+
("id", "!=", record.id), # Exclude current record being edited
127+
("state", "not in", ["cancel", "refuse"]), # Only active leaves
128+
# Check for date overlap: (start1 <= end2) and (end1 >= start2)
129+
("request_date_from", "<=", date_to),
130+
("request_date_to", ">=", date_from),
131+
]
132+
return self.search(domain)
133+
134+
def write(self, vals):
135+
"""Override write to handle automatic splitting when dates are modified."""
136+
date_from = vals.get("request_date_from")
137+
date_to = vals.get("request_date_to")
138+
139+
# If no date fields are being modified, proceed with normal write
140+
if not (date_from or date_to):
141+
return super().write(vals)
142+
143+
# For each record, check if it needs splitting after the write
144+
for record in self:
145+
# Get the final dates (new values override existing ones)
146+
final_date_from = date_from or record.request_date_from
147+
final_date_to = date_to or record.request_date_to
148+
149+
# Check if this record needs to be split
150+
if self._should_split_leave(final_date_from, final_date_to):
151+
# Convert dates for split_days method (needs datetime objects)
152+
final_date_from = self._convert_date_to_datetime(final_date_from)
153+
final_date_to = self._convert_date_to_datetime(final_date_to)
154+
155+
# Get split records
156+
split_records = self.split_days(
157+
{
158+
"request_date_from": final_date_from,
159+
"request_date_to": final_date_to,
160+
}
161+
)
162+
163+
# Only split if we actually get multiple records
164+
if len(split_records) > 1:
165+
# Prepare values for the first record (update current record)
166+
first_split = split_records[0]
167+
first_vals = vals.copy()
168+
first_vals.update(first_split)
169+
170+
# Update current record with first split period
171+
super().write(first_vals)
172+
173+
# Check for overlapping records before creating new ones
174+
records_to_create = []
175+
for split_rec in split_records[1:]:
176+
split_date_from = self._convert_to_date(split_rec["request_date_from"])
177+
split_date_to = self._convert_to_date(split_rec["request_date_to"])
178+
179+
# Check if there are overlapping records for this period
180+
overlapping = self._check_overlapping_leaves(record, split_date_from, split_date_to)
181+
182+
if not overlapping:
183+
# No overlapping records, safe to create
184+
# Use minimal required fields to avoid field errors
185+
base_vals = {
186+
"employee_id": record.employee_id.id,
187+
"holiday_status_id": record.holiday_status_id.id,
188+
}
189+
190+
# Add fields that exist in vals or record, safely
191+
safe_fields = [
192+
"number_of_days",
193+
"state",
194+
"notes",
195+
"date_from",
196+
"date_to",
197+
"name",
198+
"description",
199+
"request_unit_half",
200+
"request_unit_hours",
201+
]
202+
203+
for field in safe_fields:
204+
if field in vals:
205+
base_vals[field] = vals[field]
206+
elif hasattr(record, field):
207+
base_vals[field] = getattr(record, field, False)
208+
209+
# Add any other vals being updated (except date fields that will be overridden)
210+
for key, value in vals.items():
211+
if key not in ["request_date_from", "request_date_to"]:
212+
base_vals[key] = value
213+
214+
new_vals = base_vals.copy()
215+
new_vals.update(split_rec)
216+
records_to_create.append(new_vals)
217+
# If overlapping records exist, skip creating this split
218+
# This allows editing the current record but prevents duplicates
219+
220+
# Create only the non-overlapping records
221+
if records_to_create:
222+
self.create(records_to_create)
223+
else:
224+
# No splitting needed, process normally
225+
super().write(vals)
226+
else:
227+
# No splitting needed, process normally
228+
super().write(vals)
229+
230+
return True
231+
232+
def copy_data(self, default=None):
233+
"""Override copy_data to handle leave splitting properly."""
234+
return super().copy_data(default=default)
235+
79236
def action_approve(self, check_state=True):
80237
res = super().action_approve()
81238
if (

0 commit comments

Comments
 (0)