11import calendar
2- from datetime import timedelta
2+ from datetime import datetime , time , timedelta
33
44from odoo import api , fields , models
55
@@ -20,9 +20,18 @@ def split_days(self, rec):
2020 date_from = rec ["request_date_from" ]
2121 original_date_to = rec ["request_date_to" ]
2222
23+ # ensure working with datetime objects
24+ date_from = self ._convert_date_to_datetime (date_from )
25+ original_date_to = self ._convert_date_to_datetime (original_date_to )
26+
2327 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 )
28+ # convert to date for get_last_day_of_month, then back to datetime
29+ date_from_date = self ._convert_to_date (date_from )
30+ end_of_month_date = self .get_last_day_of_month (date_from_date )
31+
32+ # convert back to datetime
33+ end_of_month = self ._convert_date_to_datetime (end_of_month_date )
34+
2635 if original_date_to <= end_of_month :
2736 # save the dates of the single record to be created
2837 new_records .append (
@@ -40,33 +49,32 @@ def split_days(self, rec):
4049 "request_date_to" : end_of_month ,
4150 }
4251 )
43- date_from = end_of_month + timedelta (days = 1 )
52+ # Move to the first day of next month
53+ next_month_date = end_of_month_date + timedelta (days = 1 )
54+ date_from = self ._convert_date_to_datetime (next_month_date )
4455
4556 return new_records
4657
4758 @api .model_create_multi
4859 def create (self , vals_list ):
60+ """Override create to automatically split leave records across months."""
4961 new_vals_list = []
5062 for vals in vals_list :
5163 date_from = vals .get ("request_date_from" )
5264 date_to = vals .get ("request_date_to" )
5365
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 ):
66+ # if the leave spans multiple months, split it into separate records
67+ if date_from and date_to and self ._should_split_leave (date_from , date_to ):
68+ # convert to datetime objects for split_days method
69+ date_from = self ._convert_date_to_datetime (date_from )
70+ date_to = self ._convert_date_to_datetime (date_to )
6371 split_records = self .split_days (
6472 {
6573 "request_date_from" : date_from ,
6674 "request_date_to" : date_to ,
6775 }
6876 )
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
77+ # create a separate record for each split period
7078 for rec in split_records :
7179 new_vals = vals .copy ()
7280 new_vals .update (rec )
@@ -76,6 +84,143 @@ def create(self, vals_list):
7684
7785 return super ().create (new_vals_list )
7886
87+ def _convert_date_to_datetime (self , date ):
88+ """Convert string or date object to datetime object if needed."""
89+ if isinstance (date , str ):
90+ return fields .Datetime .from_string (date )
91+ elif hasattr (date , "year" ) and not hasattr (date , "hour" ):
92+ # date object: convert to datetime at start of day
93+ return datetime .combine (date , time .min )
94+ return date
95+
96+ def _convert_to_date (self , date ):
97+ """Convert datetime or string to date object for comparison."""
98+ if isinstance (date , str ):
99+ return fields .Date .from_string (date )
100+ elif hasattr (date , "date" ):
101+ # datetime object
102+ return date .date ()
103+ return date
104+
105+ def _should_split_leave (self , date_from , date_to ):
106+ """Check if leave period needs to be split across months."""
107+ # convert to date objects for month comparison
108+ date_from = self ._convert_to_date (date_from )
109+ date_to = self ._convert_to_date (date_to )
110+ last_day_of_month = self .get_last_day_of_month (date_from )
111+ return date_to > last_day_of_month
112+
113+ def _check_overlapping_leaves (self , record , date_from , date_to ):
114+ """Check if there are existing leave records that overlap with the given dates."""
115+ domain = [
116+ ("employee_id" , "=" , record .employee_id .id ),
117+ ("id" , "!=" , record .id ), # Exclude current record being edited
118+ ("state" , "not in" , ["cancel" , "refuse" ]), # Only active leaves
119+ # Check for date overlap: (start1 <= end2) and (end1 >= start2)
120+ ("request_date_from" , "<=" , date_to ),
121+ ("request_date_to" , ">=" , date_from ),
122+ ]
123+ return self .search (domain )
124+
125+ def write (self , vals ):
126+ """Override write to handle automatic splitting when dates are modified."""
127+ date_from = vals .get ("request_date_from" )
128+ date_to = vals .get ("request_date_to" )
129+
130+ # if no date fields are being modified, proceed with normal write
131+ if not (date_from or date_to ):
132+ return super ().write (vals )
133+
134+ # for each record, check if it needs splitting after the write
135+ for record in self :
136+ # get the final dates. New values override existing ones
137+ final_date_from = date_from or record .request_date_from
138+ final_date_to = date_to or record .request_date_to
139+
140+ # check if this record needs to be split
141+ if self ._should_split_leave (final_date_from , final_date_to ):
142+ # convert dates for split_days method (needs datetime objects)
143+ final_date_from = self ._convert_date_to_datetime (final_date_from )
144+ final_date_to = self ._convert_date_to_datetime (final_date_to )
145+
146+ # get split records
147+ split_records = self .split_days (
148+ {
149+ "request_date_from" : final_date_from ,
150+ "request_date_to" : final_date_to ,
151+ }
152+ )
153+
154+ # only split if we actually get multiple records
155+ if len (split_records ) > 1 :
156+ # prepare values for the first record (update current record)
157+ first_split = split_records [0 ]
158+ first_vals = vals .copy ()
159+ first_vals .update (first_split )
160+
161+ # update current record with first split period
162+ super (HrLeave , record ).write (first_vals )
163+
164+ # check for overlapping records before creating new ones
165+ records_to_create = []
166+ for split_rec in split_records [1 :]:
167+ split_date_from = self ._convert_to_date (split_rec ["request_date_from" ])
168+ split_date_to = self ._convert_to_date (split_rec ["request_date_to" ])
169+
170+ # check if there are overlapping records for this period
171+ overlapping = self ._check_overlapping_leaves (record , split_date_from , split_date_to )
172+
173+ if not overlapping :
174+ # no overlapping records, safe to create
175+ base_vals = {
176+ "employee_id" : record .employee_id .id ,
177+ "holiday_status_id" : record .holiday_status_id .id ,
178+ }
179+
180+ # add fields that exist in vals or record, safely
181+ safe_fields = [
182+ "number_of_days" ,
183+ "state" ,
184+ "notes" ,
185+ "date_from" ,
186+ "date_to" ,
187+ "name" ,
188+ "description" ,
189+ "request_unit_half" ,
190+ "request_unit_hours" ,
191+ ]
192+
193+ for field in safe_fields :
194+ if field in vals :
195+ base_vals [field ] = vals [field ]
196+ elif hasattr (record , field ):
197+ base_vals [field ] = getattr (record , field , False )
198+
199+ # add any other vals being updated (except date fields that will be overridden)
200+ for key , value in vals .items ():
201+ if key not in ["request_date_from" , "request_date_to" ]:
202+ base_vals [key ] = value
203+
204+ new_vals = base_vals .copy ()
205+ new_vals .update (split_rec )
206+ records_to_create .append (new_vals )
207+ # if overlapping records exist, skip creating this split
208+ # this allows editing the current record but prevents duplicates
209+
210+ # create only the non-overlapping records
211+ if records_to_create :
212+ self .create (records_to_create )
213+ else :
214+ super (HrLeave , record ).write (vals )
215+ else :
216+ super (HrLeave , record ).write (vals )
217+
218+ return True
219+
220+ def copy_data (self , default = None ):
221+ """Override copy_data to handle leave splitting properly."""
222+ return super ().copy_data (default = default )
223+
79224 def action_approve (self , check_state = True ):
80225 res = super ().action_approve ()
81226 if (
0 commit comments