@@ -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