diff --git a/addons/hr_work_entry_contract/models/hr_contract.py b/addons/hr_work_entry_contract/models/hr_contract.py index 21a7802f48e..79cccb69037 100644 --- a/addons/hr_work_entry_contract/models/hr_contract.py +++ b/addons/hr_work_entry_contract/models/hr_contract.py @@ -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())