Skip to content

Commit 1d53bc4

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

1 file changed

Lines changed: 170 additions & 15 deletions

File tree

hr_holidays_ux/models/hr_leave.py

Lines changed: 170 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import calendar
2-
from datetime import timedelta
2+
from datetime import datetime, time, timedelta
33

44
from odoo import api, fields, models
55

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

0 commit comments

Comments
 (0)