[IMP] hr_work_entry_contract: Make _get_contract_work_entries work in batch

Purpose
=======

Reduce by N the number of calls to the method, with N = Number of payslips
This commit is contained in:
Yannick Tivisse
2021-03-10 14:25:21 +00:00
parent b57272794c
commit d5abecbce5
@@ -44,104 +44,104 @@ class HrContract(models.Model):
return self.env.ref('hr_work_entry_contract.work_entry_type_leave')
def _get_contract_work_entries_values(self, date_start, date_stop):
self.ensure_one()
contract_vals = []
employee = self.employee_id
calendar = self.resource_calendar_id
resource = employee.resource_id
tz = pytz.timezone(calendar.tz)
start_dt = pytz.utc.localize(date_start) if not date_start.tzinfo else date_start
end_dt = pytz.utc.localize(date_stop) if not date_stop.tzinfo else date_stop
for contract in self:
employee = contract.employee_id
calendar = contract.resource_calendar_id
resource = employee.resource_id
tz = pytz.timezone(calendar.tz)
start_dt = pytz.utc.localize(date_start) if not date_start.tzinfo else date_start
end_dt = pytz.utc.localize(date_stop) if not date_stop.tzinfo else date_stop
attendances = calendar._attendance_intervals_batch(
start_dt, end_dt, resources=resource, tz=tz
)[resource.id]
attendances = calendar._attendance_intervals_batch(
start_dt, end_dt, resources=resource, tz=tz
)[resource.id]
# Other calendars: In case the employee has declared time off in another calendar
# Example: Take a time off, then a credit time.
# YTI TODO: This mimics the behavior of _leave_intervals_batch, while waiting to be cleaned
# in master.
resources_list = [self.env['resource.resource'], resource]
resource_ids = [False, resource.id]
leave_domain = [
('time_type', '=', 'leave'),
# ('calendar_id', '=', self.id), --> Get all the time offs
('resource_id', 'in', resource_ids),
('date_from', '<=', datetime_to_string(end_dt)),
('date_to', '>=', datetime_to_string(start_dt)),
('company_id', '=', self.env.company.id),
]
result = defaultdict(lambda: [])
tz_dates = {}
for leave in self.env['resource.calendar.leaves'].search(leave_domain):
for resource in resources_list:
if leave.resource_id.id not in [False, resource.id]:
# Other calendars: In case the employee has declared time off in another calendar
# Example: Take a time off, then a credit time.
# YTI TODO: This mimics the behavior of _leave_intervals_batch, while waiting to be cleaned
# in master.
resources_list = [self.env['resource.resource'], resource]
resource_ids = [False, resource.id]
leave_domain = [
('time_type', '=', 'leave'),
# ('calendar_id', '=', self.id), --> Get all the time offs
('resource_id', 'in', resource_ids),
('date_from', '<=', datetime_to_string(end_dt)),
('date_to', '>=', datetime_to_string(start_dt)),
('company_id', '=', self.env.company.id),
]
result = defaultdict(lambda: [])
tz_dates = {}
for leave in self.env['resource.calendar.leaves'].search(leave_domain):
for resource in resources_list:
if leave.resource_id.id not in [False, resource.id]:
continue
tz = tz if tz else pytz.timezone((resource or contract).tz)
if (tz, start_dt) in tz_dates:
start = tz_dates[(tz, start_dt)]
else:
start = start_dt.astimezone(tz)
tz_dates[(tz, start_dt)] = start
if (tz, end_dt) in tz_dates:
end = tz_dates[(tz, end_dt)]
else:
end = end_dt.astimezone(tz)
tz_dates[(tz, end_dt)] = end
dt0 = string_to_datetime(leave.date_from).astimezone(tz)
dt1 = string_to_datetime(leave.date_to).astimezone(tz)
result[resource.id].append((max(start, dt0), min(end, dt1), leave))
mapped_leaves = {r.id: Intervals(result[r.id]) for r in resources_list}
leaves = mapped_leaves[resource.id]
real_attendances = attendances - leaves
real_leaves = attendances - real_attendances
# A leave period can be linked to several resource.calendar.leave
split_leaves = []
for leave_interval in leaves:
if leave_interval[2] and len(leave_interval[2]) > 1:
split_leaves += [(leave_interval[0], leave_interval[1], l) for l in leave_interval[2]]
else:
split_leaves += [(leave_interval[0], leave_interval[1], leave_interval[2])]
leaves = split_leaves
# Attendances
default_work_entry_type = contract._get_default_work_entry_type()
for interval in real_attendances:
work_entry_type_id = interval[2].mapped('work_entry_type_id')[:1] or default_work_entry_type
# All benefits generated here are using datetimes converted from the employee's timezone
contract_vals += [{
'name': "%s: %s" % (work_entry_type_id.name, employee.name),
'date_start': interval[0].astimezone(pytz.utc).replace(tzinfo=None),
'date_stop': interval[1].astimezone(pytz.utc).replace(tzinfo=None),
'work_entry_type_id': work_entry_type_id.id,
'employee_id': employee.id,
'contract_id': contract.id,
'company_id': contract.company_id.id,
'state': 'draft',
}]
for interval in real_leaves:
# Could happen when a leave is configured on the interface on a day for which the
# employee is not supposed to work, i.e. no attendance_ids on the calendar.
# In that case, do try to generate an empty work entry, as this would raise a
# sql constraint error
if interval[0] == interval[1]: # if start == stop
continue
tz = tz if tz else pytz.timezone((resource or self).tz)
if (tz, start_dt) in tz_dates:
start = tz_dates[(tz, start_dt)]
else:
start = start_dt.astimezone(tz)
tz_dates[(tz, start_dt)] = start
if (tz, end_dt) in tz_dates:
end = tz_dates[(tz, end_dt)]
else:
end = end_dt.astimezone(tz)
tz_dates[(tz, end_dt)] = end
dt0 = string_to_datetime(leave.date_from).astimezone(tz)
dt1 = string_to_datetime(leave.date_to).astimezone(tz)
result[resource.id].append((max(start, dt0), min(end, dt1), leave))
mapped_leaves = {r.id: Intervals(result[r.id]) for r in resources_list}
leaves = mapped_leaves[resource.id]
real_attendances = attendances - leaves
real_leaves = attendances - real_attendances
# A leave period can be linked to several resource.calendar.leave
split_leaves = []
for leave_interval in leaves:
if leave_interval[2] and len(leave_interval[2]) > 1:
split_leaves += [(leave_interval[0], leave_interval[1], l) for l in leave_interval[2]]
else:
split_leaves += [(leave_interval[0], leave_interval[1], leave_interval[2])]
leaves = split_leaves
# Attendances
default_work_entry_type = self._get_default_work_entry_type()
for interval in real_attendances:
work_entry_type_id = interval[2].mapped('work_entry_type_id')[:1] or default_work_entry_type
# All benefits generated here are using datetimes converted from the employee's timezone
contract_vals += [{
'name': "%s: %s" % (work_entry_type_id.name, employee.name),
'date_start': interval[0].astimezone(pytz.utc).replace(tzinfo=None),
'date_stop': interval[1].astimezone(pytz.utc).replace(tzinfo=None),
'work_entry_type_id': work_entry_type_id.id,
'employee_id': employee.id,
'contract_id': self.id,
'company_id': self.company_id.id,
'state': 'draft',
}]
for interval in real_leaves:
# Could happen when a leave is configured on the interface on a day for which the
# employee is not supposed to work, i.e. no attendance_ids on the calendar.
# In that case, do try to generate an empty work entry, as this would raise a
# sql constraint error
if interval[0] == interval[1]: # if start == stop
continue
leave_entry_type = self._get_interval_leave_work_entry_type(interval, leaves)
interval_start = interval[0].astimezone(pytz.utc).replace(tzinfo=None)
interval_stop = interval[1].astimezone(pytz.utc).replace(tzinfo=None)
contract_vals += [dict([
('name', "%s%s" % (leave_entry_type.name + ": " if leave_entry_type else "", employee.name)),
('date_start', interval_start),
('date_stop', interval_stop),
('work_entry_type_id', leave_entry_type.id),
('employee_id', employee.id),
('company_id', self.company_id.id),
('state', 'draft'),
('contract_id', self.id),
] + self._get_more_vals_leave_interval(interval, leaves))]
leave_entry_type = contract._get_interval_leave_work_entry_type(interval, leaves)
interval_start = interval[0].astimezone(pytz.utc).replace(tzinfo=None)
interval_stop = interval[1].astimezone(pytz.utc).replace(tzinfo=None)
contract_vals += [dict([
('name', "%s%s" % (leave_entry_type.name + ": " if leave_entry_type else "", employee.name)),
('date_start', interval_start),
('date_stop', interval_stop),
('work_entry_type_id', leave_entry_type.id),
('employee_id', employee.id),
('company_id', contract.company_id.id),
('state', 'draft'),
('contract_id', contract.id),
] + contract._get_more_vals_leave_interval(interval, leaves))]
return contract_vals
def _get_work_entries_values(self, date_start, date_stop):
@@ -149,27 +149,23 @@ class HrContract(models.Model):
Generate a work_entries list between date_start and date_stop for one contract.
:return: list of dictionnary.
"""
vals_list = []
for contract in self:
contract_vals = contract._get_contract_work_entries_values(date_start, date_stop)
contract_vals = self._get_contract_work_entries_values(date_start, date_stop)
for contract in self:
# If we generate work_entries which exceeds date_start or date_stop, we change boundaries on contract
if contract_vals:
date_stop_max = max([x['date_stop'] for x in contract_vals])
date_stop_max = max([x['date_stop'] for x in contract_vals if x['contract_id'] == contract.id])
if date_stop_max > contract.date_generated_to:
contract.date_generated_to = date_stop_max
date_start_min = min([x['date_start'] for x in contract_vals])
date_start_min = min([x['date_start'] for x in contract_vals if x['contract_id'] == contract.id])
if date_start_min < contract.date_generated_from:
contract.date_generated_from = date_start_min
vals_list += contract_vals
return vals_list
return contract_vals
def _generate_work_entries(self, date_start, date_stop, force=False):
vals_list = []
date_start = fields.Datetime.to_datetime(date_start)
date_stop = datetime.combine(fields.Datetime.to_datetime(date_stop), datetime.max.time())