[FIX] hr_payroll: calculation of work days
The current calculation of work days is inconsistent with the leaves of the employee in several cases. The core of the problem is the logic of the algorithm: for a given day, if we can find one hr_holiday within this day, we consider it as a leave. One can easily spot several issues with the existing logic: - if an employee takes less than a day-off (e.g. a half day), the complete day is considered as a leave. - the timezone (TZ) is not taken into account when searching for a leave, while it is when computing the number of working hours. A leave from 3:00 AM to 3:00 PM in the Los Angeles timezone would then be considered as two days-off since it spread on two days in UTC. The logic is therefore completely modified. For each day of the period: - compute the work intervals (TZ taken into account, converted in UTC) - search for a hr.holiday which completely includes the work interval - if a holiday is found, the work interval is considered as a leave. Otherwise, it is considered as a normal work period. This is a first step in improving the logic. The first faulty use case is partially solved, since we cover now complete work intervals. However, a leave covering only partially a work interval will not be taken into account. The second faulty use case is solved. opw-707897
This commit is contained in:
@@ -308,19 +308,25 @@ class HrPayslip(models.Model):
|
||||
@param contract_ids: list of contract id
|
||||
@return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to
|
||||
"""
|
||||
def was_on_leave(employee_id, datetime_day):
|
||||
day = fields.Date.to_string(datetime_day)
|
||||
|
||||
def was_on_leave_interval(employee_id, date_from, date_to):
|
||||
date_from = fields.Datetime.to_string(date_from)
|
||||
date_to = fields.Datetime.to_string(date_to)
|
||||
return self.env['hr.holidays'].search([
|
||||
('state', '=', 'validate'),
|
||||
('employee_id', '=', employee_id),
|
||||
('type', '=', 'remove'),
|
||||
('date_from', '<=', day),
|
||||
('date_to', '>=', day)
|
||||
], limit=1).holiday_status_id.name
|
||||
('date_from', '<=', date_from),
|
||||
('date_to', '>=', date_to)
|
||||
], limit=1)
|
||||
|
||||
res = []
|
||||
#fill only if the contract as a working schedule linked
|
||||
uom_day = self.env.ref('product.product_uom_day', raise_if_not_found=False)
|
||||
for contract in self.env['hr.contract'].browse(contract_ids).filtered(lambda contract: contract.working_hours):
|
||||
uom_hour = contract.employee_id.resource_id.calendar_id.uom_id or self.env.ref('product.product_uom_hour', raise_if_not_found=False)
|
||||
interval_data = []
|
||||
holidays = self.env['hr.holidays']
|
||||
attendances = {
|
||||
'name': _("Normal Working Days paid at 100%"),
|
||||
'sequence': 1,
|
||||
@@ -333,31 +339,43 @@ class HrPayslip(models.Model):
|
||||
day_from = fields.Datetime.from_string(date_from)
|
||||
day_to = fields.Datetime.from_string(date_to)
|
||||
nb_of_days = (day_to - day_from).days + 1
|
||||
|
||||
# Gather all intervals and holidays
|
||||
for day in range(0, nb_of_days):
|
||||
working_hours_on_day = contract.working_hours.working_hours_on_day(day_from + timedelta(days=day))
|
||||
if working_hours_on_day:
|
||||
#the employee had to work
|
||||
leave_type = was_on_leave(contract.employee_id.id, day_from + timedelta(days=day))
|
||||
if leave_type:
|
||||
#if he was on leave, fill the leaves dict
|
||||
if leave_type in leaves:
|
||||
leaves[leave_type]['number_of_days'] += 1.0
|
||||
leaves[leave_type]['number_of_hours'] += working_hours_on_day
|
||||
else:
|
||||
leaves[leave_type] = {
|
||||
'name': leave_type,
|
||||
'sequence': 5,
|
||||
'code': leave_type,
|
||||
'number_of_days': 1.0,
|
||||
'number_of_hours': working_hours_on_day,
|
||||
'contract_id': contract.id,
|
||||
}
|
||||
working_intervals_on_day = contract.working_hours.get_working_intervals_of_day(start_dt=day_from + timedelta(days=day))
|
||||
for interval in working_intervals_on_day:
|
||||
interval_data.append((interval, was_on_leave_interval(contract.employee_id.id, interval[0], interval[1])))
|
||||
|
||||
# Extract information from previous data. A working interval is considered:
|
||||
# - as a leave if a hr.holiday completely covers the period
|
||||
# - as a working period instead
|
||||
for interval, holiday in interval_data:
|
||||
holidays |= holiday
|
||||
hours = (interval[1] - interval[0]).total_seconds() / 3600.0
|
||||
if holiday:
|
||||
#if he was on leave, fill the leaves dict
|
||||
if holiday.holiday_status_id.name in leaves:
|
||||
leaves[holiday.holiday_status_id.name]['number_of_hours'] += hours
|
||||
else:
|
||||
#add the input vals to tmp (increment if existing)
|
||||
attendances['number_of_days'] += 1.0
|
||||
attendances['number_of_hours'] += working_hours_on_day
|
||||
leaves[holiday.holiday_status_id.name] = {
|
||||
'name': holiday.holiday_status_id.name,
|
||||
'sequence': 5,
|
||||
'code': holiday.holiday_status_id.name,
|
||||
'number_of_days': 0.0,
|
||||
'number_of_hours': hours,
|
||||
'contract_id': contract.id,
|
||||
}
|
||||
else:
|
||||
#add the input vals to tmp (increment if existing)
|
||||
attendances['number_of_hours'] += hours
|
||||
|
||||
# Clean-up the results
|
||||
leaves = [value for key, value in leaves.items()]
|
||||
res += [attendances] + leaves
|
||||
for data in [attendances] + leaves:
|
||||
data['number_of_days'] = uom_hour._compute_quantity(data['number_of_hours'], uom_day)\
|
||||
if uom_day and uom_hour\
|
||||
else data['number_of_hours'] / 8.0
|
||||
res.append(data)
|
||||
return res
|
||||
|
||||
# YTI TODO contract_ids should be a browse record
|
||||
|
||||
Reference in New Issue
Block a user