11import calendar
2- from datetime import timedelta
2+ from datetime import datetime , time , timedelta
33
44from 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