Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 159 additions & 14 deletions hr_holidays_ux/models/hr_leave.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import calendar
from datetime import timedelta
from datetime import datetime, time, timedelta

from odoo import api, fields, models

Expand All @@ -20,9 +20,18 @@ def split_days(self, rec):
date_from = rec["request_date_from"]
original_date_to = rec["request_date_to"]

# ensure working with datetime objects
date_from = self._convert_date_to_datetime(date_from)
original_date_to = self._convert_date_to_datetime(original_date_to)

while date_from <= original_date_to:
# get last day of the month
end_of_month = self.get_last_day_of_month(date_from)
# convert to date for get_last_day_of_month, then back to datetime
date_from_date = self._convert_to_date(date_from)
end_of_month_date = self.get_last_day_of_month(date_from_date)

# convert back to datetime
end_of_month = self._convert_date_to_datetime(end_of_month_date)

if original_date_to <= end_of_month:
# save the dates of the single record to be created
new_records.append(
Expand All @@ -40,33 +49,32 @@ def split_days(self, rec):
"request_date_to": end_of_month,
}
)
date_from = end_of_month + timedelta(days=1)
# Move to the first day of next month
next_month_date = end_of_month_date + timedelta(days=1)
date_from = self._convert_date_to_datetime(next_month_date)

return new_records

@api.model_create_multi
def create(self, vals_list):
"""Override create to automatically split leave records across months."""
new_vals_list = []
for vals in vals_list:
date_from = vals.get("request_date_from")
date_to = vals.get("request_date_to")

if date_from and date_to:
# convert to datetime objects if they are strings
if isinstance(date_from, str):
date_from = fields.Datetime.from_string(date_from)
if isinstance(date_to, str):
date_to = fields.Datetime.from_string(date_to)

# if the leave ends in a different month, call the split_days() method to get the dates for splitting the leaves
if date_to > self.get_last_day_of_month(date_from):
# if the leave spans multiple months, split it into separate records
if date_from and date_to and self._should_split_leave(date_from, date_to):
# convert to datetime objects for split_days method
date_from = self._convert_date_to_datetime(date_from)
date_to = self._convert_date_to_datetime(date_to)
split_records = self.split_days(
{
"request_date_from": date_from,
"request_date_to": date_to,
}
)
# 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
# create a separate record for each split period
for rec in split_records:
new_vals = vals.copy()
new_vals.update(rec)
Expand All @@ -76,6 +84,143 @@ def create(self, vals_list):

return super().create(new_vals_list)

def _convert_date_to_datetime(self, date):
"""Convert string or date object to datetime object if needed."""
if isinstance(date, str):
return fields.Datetime.from_string(date)
elif hasattr(date, "year") and not hasattr(date, "hour"):
# date object: convert to datetime at start of day
return datetime.combine(date, time.min)
return date

def _convert_to_date(self, date):
"""Convert datetime or string to date object for comparison."""
if isinstance(date, str):
return fields.Date.from_string(date)
elif hasattr(date, "date"):
# datetime object
return date.date()
return date

def _should_split_leave(self, date_from, date_to):
"""Check if leave period needs to be split across months."""
# convert to date objects for month comparison
date_from = self._convert_to_date(date_from)
date_to = self._convert_to_date(date_to)
last_day_of_month = self.get_last_day_of_month(date_from)
return date_to > last_day_of_month
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quizas no son necesarias las variables y se puede hacer directamente la comparación


def _check_overlapping_leaves(self, record, date_from, date_to):
"""Check if there are existing leave records that overlap with the given dates."""
domain = [
("employee_id", "=", record.employee_id.id),
("id", "!=", record.id), # Exclude current record being edited
("state", "not in", ["cancel", "refuse"]), # Only active leaves
# Check for date overlap: (start1 <= end2) and (end1 >= start2)
("request_date_from", "<=", date_to),
("request_date_to", ">=", date_from),
]
return self.search(domain)

def write(self, vals):
"""Override write to handle automatic splitting when dates are modified."""
date_from = vals.get("request_date_from")
date_to = vals.get("request_date_to")

# if no date fields are being modified, proceed with normal write
if not (date_from or date_to):
return super().write(vals)

# for each record, check if it needs splitting after the write
for record in self:
# get the final dates. New values override existing ones
final_date_from = date_from or record.request_date_from
final_date_to = date_to or record.request_date_to

# check if this record needs to be split
if self._should_split_leave(final_date_from, final_date_to):
# convert dates for split_days method (needs datetime objects)
final_date_from = self._convert_date_to_datetime(final_date_from)
final_date_to = self._convert_date_to_datetime(final_date_to)

# get split records
split_records = self.split_days(
{
"request_date_from": final_date_from,
"request_date_to": final_date_to,
}
)

# only split if we actually get multiple records
if len(split_records) > 1:
# prepare values for the first record (update current record)
first_split = split_records[0]
first_vals = vals.copy()
first_vals.update(first_split)

# update current record with first split period
super(HrLeave, record).write(first_vals)

# check for overlapping records before creating new ones
records_to_create = []
for split_rec in split_records[1:]:
split_date_from = self._convert_to_date(split_rec["request_date_from"])
split_date_to = self._convert_to_date(split_rec["request_date_to"])

# check if there are overlapping records for this period
overlapping = self._check_overlapping_leaves(record, split_date_from, split_date_to)

if not overlapping:
# no overlapping records, safe to create
base_vals = {
"employee_id": record.employee_id.id,
"holiday_status_id": record.holiday_status_id.id,
}

# add fields that exist in vals or record, safely
safe_fields = [
"number_of_days",
"state",
"notes",
"date_from",
"date_to",
"name",
"description",
"request_unit_half",
"request_unit_hours",
]

for field in safe_fields:
if field in vals:
base_vals[field] = vals[field]
elif hasattr(record, field):
base_vals[field] = getattr(record, field, False)

# add any other vals being updated (except date fields that will be overridden)
for key, value in vals.items():
if key not in ["request_date_from", "request_date_to"]:
base_vals[key] = value

new_vals = base_vals.copy()
new_vals.update(split_rec)
records_to_create.append(new_vals)
# if overlapping records exist, skip creating this split
# this allows editing the current record but prevents duplicates

# create only the non-overlapping records
if records_to_create:
self.create(records_to_create)
else:
super(HrLeave, record).write(vals)
else:
super(HrLeave, record).write(vals)

return True

def copy_data(self, default=None):
"""Override copy_data to handle leave splitting properly."""
return super().copy_data(default=default)

def action_approve(self, check_state=True):
res = super().action_approve()
if (
Expand Down