def _cancel_benefit_conflict(self): """ Unlink any benefit linked to a leave in self. Re-create new benefits where the leaves do not cover the full range of the deleted benefits. Create a leave benefit for each leave in self. Return True if one or more benefits are unlinked. e.g.: |---------------- benefit ----------------| |------ leave ------| || vv |-benef-|---benefit leave---|----benefit---| """ benefits = self.env['hr.benefit'].search([('leave_id', 'in', self.ids) ]) if benefits: self.copy_to_benefits() # create new benefits where the leave does not cover the full benefit benefits_intervals = Intervals(intervals=[(b.date_start, b.date_stop, b) for b in benefits]) leave_intervals = Intervals(intervals=[(l.date_from, l.date_to, l) for l in self]) remaining_benefits = benefits_intervals - leave_intervals for interval in remaining_benefits: benefit = interval[2] leave = benefit.leave_id benefit_type = benefit.benefit_type_id employee = benefit.employee_id benefit_start = interval[0] + relativedelta( seconds=1) if leave.date_to == interval[0] else interval[0] benefit_stop = interval[1] - relativedelta( seconds=1 ) if leave.date_from == interval[1] else interval[1] self.env['hr.benefit'].safe_duplicate_create({ 'name': "%s: %s" % (benefit_type.name, employee.name), 'date_start': benefit_start, 'date_stop': benefit_stop, 'benefit_type_id': benefit_type.id, 'contract_id': benefit.contract_id.id, 'employee_id': employee.id, 'state': 'confirmed', }) benefits.unlink() return True return False
def _public_holidays_leave_intervals(self, start_dt, end_dt, employee_id, tz): """Get the public holidays for the current employee and given dates in the format expected by resource methods. :param: start_dt: Initial datetime. :param: end_dt: End datetime. :param: employee_id: Employee ID. It can be false. :return: List of tuples with (start_date, end_date) as elements. """ HrHolidaysPublic = self.env['hr.holidays.public'] leaves = [] if start_dt.year != end_dt.year: # This fixes the case of leave request asked over 2 years. # # adding 1 year to end_dt for rrule to retrieve correct years for # public holidays to work # rrule.rrule(rrule.YEARLY, dtstart=2019-12-22, until=2020-01-05) # gives [2019] # rrule.rrule(rrule.YEARLY, dtstart=2019-12-22, until=2021-01-05) # gives [2019, 2020] end_dt = end_dt.replace(year=end_dt.year + 1) for day in rrule.rrule(rrule.YEARLY, dtstart=start_dt, until=end_dt): lines = HrHolidaysPublic.get_holidays_list( day.year, employee_id=employee_id, ) for line in lines: leaves.append( (datetime.combine(line.date, time.min).replace(tzinfo=tz), datetime.combine(line.date, time.max).replace(tzinfo=tz), line), ) return Intervals(leaves)
def _get_calendars_validity_within_period(self, start, end, default_company=None): """ Returns a dict of dict with resource's id as first key and resource's calendar as secondary key The value is the validity interval of the calendar for the given resource. The validity interval of the employee resource calendar is the lifetime of the employee, from creation to departure. """ assert start.tzinfo and end.tzinfo calendars_within_period_per_resource = super( )._get_calendars_validity_within_period( start, end, default_company=default_company) for resource in self: if not resource.employee_id: continue create_date = max(start, utc.localize(resource.employee_id.create_date)) if resource.employee_id.departure_date and resource.employee_id.departure_date <= end.date( ): departure_datetime = timezone(resource.tz).localize( datetime.combine(resource.employee_id.departure_date, datetime.max.time())) departure_datetime = min(departure_datetime, end) else: departure_datetime = end interval = Intervals([(create_date, departure_datetime, self.env['resource.calendar.attendance'])]) for calendar in calendars_within_period_per_resource[resource.id]: calendars_within_period_per_resource[ resource.id][calendar] &= interval return calendars_within_period_per_resource
def _get_first_available_slot(self, start_datetime, duration): """Get the first available interval for the workcenter in `self`. The available interval is disjoinct with all other workorders planned on this workcenter, but can overlap the time-off of the related calendar (inverse of the working hours). Return the first available interval (start datetime, end datetime) or, if there is none before 700 days, a tuple error (False, 'error message'). :param start_datetime: begin the search at this datetime :param duration: minutes needed to make the workorder (float) :rtype: tuple """ self.ensure_one() start_datetime, revert = make_aware(start_datetime) resource = self.resource_id get_available_intervals = partial( self.resource_calendar_id._work_intervals_batch, domain=[('time_type', 'in', ['other', 'leave'])], resources=resource, tz=timezone(self.resource_calendar_id.tz)) get_workorder_intervals = partial( self.resource_calendar_id._leave_intervals_batch, domain=[('time_type', '=', 'other')], resources=resource, tz=timezone(self.resource_calendar_id.tz)) remaining = duration start_interval = start_datetime delta = timedelta(days=14) for n in range(50): # 50 * 14 = 700 days in advance (hardcoded) dt = start_datetime + delta * n available_intervals = get_available_intervals(dt, dt + delta)[resource.id] workorder_intervals = get_workorder_intervals(dt, dt + delta)[resource.id] for start, stop, dummy in available_intervals: # Shouldn't loop more than 2 times because the available_intervals contains the workorder_intervals # And remaining == duration can only occur at the first loop and at the interval intersection (cannot happen several time because available_intervals > workorder_intervals for _i in range(2): interval_minutes = (stop - start).total_seconds() / 60 # If the remaining minutes has never decrease update start_interval if remaining == duration: start_interval = start # If there is a overlap between the possible available interval and a others WO if Intervals( [(start_interval, start + timedelta(minutes=min(remaining, interval_minutes)), dummy)]) & workorder_intervals: remaining = duration elif float_compare(interval_minutes, remaining, precision_digits=3) >= 0: return revert(start_interval), revert( start + timedelta(minutes=remaining)) else: # Decrease a part of the remaining duration remaining -= interval_minutes return False, 'Not available slot 700 days after the planned start'
def _cancel_benefit_conflict(self): benefits = self.env['hr.benefit'].search([('leave_id', 'in', self.ids) ]) if benefits: self.copy_to_benefits() # create new benefits where the leave does not cover the full benefit benefits_intervals = Intervals(intervals=[(b.date_start, b.date_stop, b) for b in benefits]) leave_intervals = Intervals(intervals=[(l.date_from, l.date_to, l) for l in self]) remaining_benefits = benefits_intervals - leave_intervals for interval in remaining_benefits: benefit = interval[2] leave = benefit.leave_id benefit_type = benefit.benefit_type_id employee = benefit.employee_id benefit_start = interval[0] + relativedelta( seconds=1) if leave.date_to == interval[0] else interval[0] benefit_stop = interval[1] - relativedelta( seconds=1 ) if leave.date_from == interval[1] else interval[1] self.env['hr.benefit'].safe_duplicate_create({ 'name': "%s: %s" % (benefit_type.name, employee.name), 'date_start': benefit_start, 'date_stop': benefit_stop, 'benefit_type_id': benefit_type.id, 'employee_id': employee.id, 'state': 'confirmed', }) benefits.unlink()
def test_calendars_validity_within_period(self): tz = timezone(self.employee.tz) calendars = self.employee.resource_id._get_calendars_validity_within_period( tz.localize(datetime(2021, 10, 1, 0, 0, 0)), tz.localize(datetime(2021, 12, 1, 0, 0, 0)), ) interval_35h = Intervals([ (tz.localize(datetime(2021, 10, 1, 0, 0, 0)), tz.localize( datetime.combine(date(2021, 10, 31), datetime.max.time())), self.env['resource.calendar.attendance']) ]) interval_40h = Intervals([(tz.localize(datetime(2021, 11, 1, 0, 0, 0)), tz.localize(datetime(2021, 12, 1, 0, 0, 0)), self.env['resource.calendar.attendance'])]) self.assertEqual( 1, len(calendars), "The dict returned by calendars validity should only have 1 entry") self.assertEqual(2, len(calendars[self.employee.resource_id.id]), "Jean should only have one calendar") richard_entries = calendars[self.employee.resource_id.id] for calendar in richard_entries: self.assertTrue( calendar in (self.calendar_35h | self.calendar_richard), "Each calendar should be listed") if calendar == self.calendar_35h: self.assertFalse( richard_entries[calendar] - interval_35h, "Interval 35h should cover all calendar 35h validity") self.assertFalse( interval_35h - richard_entries[calendar], "Calendar 35h validity should cover all interval 35h") elif calendar == self.calendar_richard: self.assertFalse( richard_entries[calendar] - interval_40h, "Interval 40h should cover all calendar 40h validity") self.assertFalse( interval_40h - richard_entries[calendar], "Calendar 40h validity should cover all interval 40h")
def test_calendars_validity_within_period_creation(self): calendars = self.employee_niv.resource_id._get_calendars_validity_within_period( utc.localize(datetime(2020, 12, 1, 8, 0, 0)), utc.localize(datetime(2021, 1, 31, 17, 0, 0)), ) interval = Intervals([(utc.localize(datetime(2021, 1, 1, 10, 0, 0)), utc.localize(datetime(2021, 1, 31, 17, 0, 0)), self.env['resource.calendar.attendance'])]) niv_entry = calendars[self.employee_niv.resource_id.id] self.assertFalse(niv_entry[self.calendar_40h] - interval, "Interval should cover all calendar's validity") self.assertFalse(interval - niv_entry[self.calendar_40h], "Calendar validity should cover all interval")
def test_calendars_validity_within_period_before_departure(self): calendars = self.employee_niv.resource_id._get_calendars_validity_within_period( utc.localize(datetime(2022, 5, 1, 8, 0, 0)), utc.localize(datetime(2022, 6, 30, 17, 0, 0)), ) interval = Intervals([(utc.localize(datetime(2022, 5, 1, 8, 0, 0)), timezone(self.employee_niv.tz).localize( datetime(2022, 6, 1, 23, 59, 59, 999999)), self.env['resource.calendar.attendance'])]) niv_entry = calendars[self.employee_niv.resource_id.id] self.assertFalse(niv_entry[self.calendar_40h] - interval, "Interval should cover all calendar's validity") self.assertFalse(interval - niv_entry[self.calendar_40h], "Calendar validity should cover all interval")
def _mark_conflicting_benefits(self, start, stop): conflict = False domain = [ ('date_start', '<', stop), ('date_stop', '>', start), ] benefs = self.search(domain) benefits_by_employee = itertools.groupby(benefs, lambda b: b.employee_id) for employee, benefs in benefits_by_employee: intervals = Intervals(intervals=((b.date_start, b.date_stop, b) for b in benefs)) for interval in intervals: if len(interval[2]) > 1: interval[2].write({'display_warning': True}) conflict = True return conflict
def _compute_schedule_conflicts(self): conflict = False date_start_benefits = min(self.mapped('date_start')) date_stop_benefits = max(self.mapped('date_stop')) domain = [ ('date_start', '<', date_stop_benefits), ('date_stop', '>', date_start_benefits), ] benefs = self.search(domain) benefits_by_employee = itertools.groupby(benefs, lambda b: b.employee_id) for employee, benefs in benefits_by_employee: intervals = Intervals(intervals=((b.date_start, b.date_stop, b) for b in benefs)) for interval in intervals: if len(interval[2]) > 1: interval[2].write({'display_warning': True}) conflict = True return conflict
def _natural_period_intervals_batch(self, start_dt, end_dt, intervals, resources): for resource in resources: interval_resource = intervals[resource.id] tz = timezone(resource.tz) attendances = [] if len(interval_resource._items) > 0: attendances = interval_resource._items for day in rrule.rrule(rrule.DAILY, dtstart=start_dt, until=end_dt): exist_interval = self._exist_interval_in_date(attendances, day.date()) if not exist_interval: attendances.append( ( datetime.combine(day.date(), time.min).replace(tzinfo=tz), datetime.combine(day.date(), time.max).replace(tzinfo=tz), self.env["resource.calendar.attendance"], ) ) intervals[resource.id] = Intervals(attendances) return intervals
def _public_holidays_leave_intervals(self, start_dt, end_dt, employee_id, tz): """Get the public holidays for the current employee and given dates in the format expected by resource methods. :param: start_dt: Initial datetime. :param: end_dt: End datetime. :param: employee_id: Employee ID. It can be false. :return: List of tuples with (start_date, end_date) as elements. """ HrHolidaysPublic = self.env["hr.holidays.public"] leaves = [] if start_dt.year != end_dt.year: # This fixes the case of leave request asked over 2 years. # # adding 1 year to end_dt for rrule to retrieve correct years for # public holidays to work # rrule.rrule(rrule.YEARLY, dtstart=2019-12-22, until=2020-01-05) # gives [2019] # rrule.rrule(rrule.YEARLY, dtstart=2019-12-22, until=2021-01-05) # gives [2019, 2020] end_dt = end_dt.replace(year=end_dt.year + 1) for day in rrule.rrule(rrule.YEARLY, dtstart=start_dt, until=end_dt): lines = HrHolidaysPublic.get_holidays_list(day.year, employee_id=employee_id) # In some cases, an error appears about mixing 2 models # (hr.holidays.public.line + resource.calendar.leaves) # in _leave_intervals or _leave_intervals_batch functions # It only happen when both holidays and leaves exist. # The solution is to pass an empty leave in the tuple instead # of the public holiday line record, as this element has no # further use except the union operation. resource_leave_model = self.env["resource.calendar.leaves"] for line in lines: leaves.append(( datetime.combine(line.date, time.min).replace(tzinfo=tz), datetime.combine(line.date, time.max).replace(tzinfo=tz), resource_leave_model, )) return Intervals(leaves)
def _attendance_intervals_batch_exclude_public_holidays( self, start_dt, end_dt, intervals, resources, tz ): list_by_dates = ( self.env["hr.holidays.public"] .get_holidays_list( start_dt=start_dt.date(), end_dt=end_dt.date(), employee_id=self.env.context.get("employee_id", False), ) .mapped("date") ) for resource in resources: interval_resource = intervals[resource.id] attendances = [] for attendance in interval_resource._items: if attendance[0].date() not in list_by_dates: attendances.append(attendance) intervals[resource.id] = Intervals(attendances) return intervals
def test_calendars_validity_within_period_default(self): calendars = self.employee_niv.resource_id._get_calendars_validity_within_period( utc.localize(datetime(2021, 7, 1, 8, 0, 0)), utc.localize(datetime(2021, 7, 30, 17, 0, 0)), ) interval = Intervals([(utc.localize(datetime(2021, 7, 1, 8, 0, 0)), utc.localize(datetime(2021, 7, 30, 17, 0, 0)), self.env['resource.calendar.attendance'])]) self.assertEqual( 1, len(calendars), "The dict returned by calendars validity should only have 1 entry") self.assertEqual(1, len(calendars[self.employee_niv.resource_id.id]), "Niv should only have one calendar") niv_entry = calendars[self.employee_niv.resource_id.id] niv_calendar = next(iter(niv_entry)) self.assertEqual(niv_calendar, self.calendar_40h, "It should be Niv's Calendar") self.assertFalse(niv_entry[niv_calendar] - interval, "Interval should cover all calendar's validity") self.assertFalse(interval - niv_entry[niv_calendar], "Calendar validity should cover all interval")
def _weekend_intervals(self, start_dt, end_dt, resource=None): """ Return the weekend intervals in the given datetime range. The returned intervals are expressed in the resource's timezone. """ tz = timezone((resource or self).tz) start_dt = start_dt.astimezone(tz) end_dt = end_dt.astimezone(tz) start = start_dt.date() until = end_dt.date() result = [] weekdays = [ int(attendance.dayofweek) for attendance in self.attendance_ids ] weekends = [d for d in range(7) if d not in weekdays] for day in rrule.rrule(rrule.DAILY, start, until=until, byweekday=weekends): result.append( (datetime.combine(day, time.min).astimezone(tz), datetime.combine(day, time.max).astimezone(tz), self), ) return Intervals(result)
def _public_holidays_leave_intervals(self, start_dt, end_dt, employee_id, tz): """Get the public holidays for the current employee and given dates in the format expected by resource methods. :param: start_dt: Initial datetime. :param: end_dt: End datetime. :param: employee_id: Employee ID. It can be false. :return: List of tuples with (start_date, end_date) as elements. """ HrHolidaysPublic = self.env['hr.holidays.public'] leaves = [] for day in rrule.rrule(rrule.YEARLY, dtstart=start_dt, until=end_dt): lines = HrHolidaysPublic.get_holidays_list( day.year, employee_id=employee_id, ) for line in lines: leaves.append( (datetime.combine(line.date, time.min).replace(tzinfo=tz), datetime.combine(line.date, time.max).replace(tzinfo=tz), line), ) return Intervals(leaves)
def _get_calendars_validity_within_period(self, start, end, default_company=None): assert start.tzinfo and end.tzinfo if not self: return super()._get_calendars_validity_within_period(start, end, default_company=default_company) calendars_within_period_per_resource = defaultdict(lambda: defaultdict(Intervals)) # keys are [resource id:integer][calendar:self.env['resource.calendar']] resource_without_contract = self.filtered(lambda r: not r.employee_id or r.employee_id.employee_type not in ['employee', 'student']) if resource_without_contract: calendars_within_period_per_resource.update( super(ResourceResource, resource_without_contract)._get_calendars_validity_within_period(start, end, default_company=default_company) ) resource_with_contract = self - resource_without_contract if not resource_with_contract: return calendars_within_period_per_resource timezones = {resource.tz for resource in resource_with_contract} date_start = min(start.astimezone(timezone(tz)).date() for tz in timezones) date_end = max(end.astimezone(timezone(tz)).date() for tz in timezones) contracts = resource_with_contract.employee_id._get_contracts( date_start, date_end, states=['open', 'draft', 'close'] ).filtered(lambda c: c.state in ['open', 'close'] or c.kanban_state == 'done') for contract in contracts: tz = timezone(contract.employee_id.tz) calendars_within_period_per_resource[contract.employee_id.resource_id.id][contract.resource_calendar_id] |= Intervals([( tz.localize(datetime.combine(contract.date_start, datetime.min.time())) if contract.date_start > start.astimezone(tz).date() else start, tz.localize(datetime.combine(contract.date_end, datetime.max.time())) if contract.date_end and contract.date_end < end.astimezone(tz).date() else end, self.env['resource.calendar.attendance'] )]) return calendars_within_period_per_resource
def _get_contract_work_entries_values(self, date_start, date_stop): contract_vals = [] bypassing_work_entry_type_codes = self._get_bypassing_work_entry_type_codes() 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] # 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 leave_entry_type = contract._get_interval_leave_work_entry_type(interval, leaves, bypassing_work_entry_type_codes) 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 check(a, b, c): a, b, c = self.ints(a), self.ints(b), self.ints(c) self.assertEqual(list(Intervals(a) - Intervals(b)), c)
def _get_contract_work_entries_values(self, date_start, date_stop): 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 contract_vals = [] bypassing_work_entry_type_codes = self._get_bypassing_work_entry_type_codes() attendances_by_resource = self._get_attendance_intervals(start_dt, end_dt) resource_calendar_leaves = self.env['resource.calendar.leaves'].search(self._get_leave_domain(start_dt, end_dt)) # {resource: resource_calendar_leaves} leaves_by_resource = defaultdict(lambda: self.env['resource.calendar.leaves']) for leave in resource_calendar_leaves: leaves_by_resource[leave.resource_id.id] |= leave tz_dates = {} for contract in self: employee = contract.employee_id calendar = contract.resource_calendar_id resource = employee.resource_id tz = pytz.timezone(calendar.tz) attendances = attendances_by_resource[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] result = defaultdict(lambda: []) for leave in itertools.chain(leaves_by_resource[False], leaves_by_resource[resource.id]): for resource in resources_list: # Global time off is not for this calendar, can happen with multiple calendars in self if resource and leave.calendar_id and leave.calendar_id != calendar and not leave.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 if contract.has_static_work_entries() or not leaves: # Empty leaves means empty real_leaves real_leaves = attendances - real_attendances else: # In the case of attendance based contracts use regular attendances to generate leave intervals static_attendances = calendar._attendance_intervals_batch( start_dt, end_dt, resources=resource, tz=tz)[resource.id] real_leaves = static_attendances & leaves if not contract.has_static_work_entries(): # An attendance based contract might have an invalid planning, by definition it may not happen with # static work entries. # Creating overlapping slots for example might lead to a single work entry. # In that case we still create both work entries to indicate a problem (conflicting W E). split_attendances = [] for attendance in real_attendances: if attendance[2] and len(attendance[2]) > 1: split_attendances += [(attendance[0], attendance[1], a) for a in attendance[2]] else: split_attendances += [attendance] real_attendances = split_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 = 'work_entry_type_id' in interval[2] and interval[2].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 += [dict([ ('name', "%s: %s" % (work_entry_type.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), ('employee_id', employee.id), ('contract_id', contract.id), ('company_id', contract.company_id.id), ('state', 'draft'), ] + contract._get_more_vals_attendance_interval(interval))] 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 = contract._get_interval_leave_work_entry_type(interval, leaves, bypassing_work_entry_type_codes) 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 check(a, b): a, b = self.ints(a), self.ints(b) self.assertEqual(list(Intervals(a)), b)