Example #1
0
    def _new_schedule_hours(self,
                            hours,
                            day_dt,
                            compute_leaves=False,
                            resource_id=None):
        if not hasattr(self, '_get_schedule_hours_iteration_limit'):
            return self._schedule_hours_original(hours,
                                                 day_dt,
                                                 compute_leaves=compute_leaves,
                                                 resource_id=resource_id)
        self.ensure_one()
        # START OF HOOK: Introduce here the iterations limit
        iterations_limit = self._get_schedule_hours_iteration_limit() or 1000
        # END OF HOOK
        backwards = (hours < 0)
        intervals = []
        remaining_hours, iterations = abs(hours * 1.0), 0

        day_dt_tz = to_naive_user_tz(day_dt, self.env.user)
        current_datetime = day_dt_tz

        call_args = dict(compute_leaves=compute_leaves,
                         resource_id=resource_id)

        # HOOK. Use the iterations_limit here
        while float_compare(remaining_hours, 0.0, precision_digits=2) in (
                1, 0) and iterations < iterations_limit:
            if backwards:
                call_args['end_time'] = current_datetime.time()
            else:
                call_args['start_time'] = current_datetime.time()

            working_intervals = self._get_day_work_intervals(
                current_datetime.date(), **call_args)

            if working_intervals:
                new_working_intervals = self._interval_schedule_hours(
                    working_intervals, remaining_hours, backwards=backwards)

                res = timedelta()
                for interval in working_intervals:
                    res += interval[1] - interval[0]
                remaining_hours -= res.total_seconds() / 3600.0

                intervals = intervals + new_working_intervals if \
                    not backwards else new_working_intervals + intervals
            # get next day
            if backwards:
                current_datetime = datetime.datetime.combine(
                    self._get_previous_work_day(current_datetime),
                    datetime.time(23, 59, 59))
            else:
                current_datetime = datetime.datetime.combine(
                    self._get_next_work_day(current_datetime), datetime.time())
            # avoid infinite loops
            iterations += 1

        return intervals
Example #2
0
 def test_calendar_days_scheduling_timezone(self):
     self.env.user.tz = 'US/Alaska'
     res = self.calendar.plan_days(
         5,
         to_naive_utc(Datetime.from_string('2013-02-12 09:08:07'),
                      self.env.user))
     self.assertEqual(
         to_naive_user_tz(res, self.env.user).date(),
         Datetime.from_string('2013-02-26 00:00:00').date())
Example #3
0
    def _set_check_in_out(self):
        signin = signout = False
        self.early_hours = 0.0
        self.late_hours = 0.0
        self.worked_hours = 0.0
        duty_from = self.date + ' ' + str(
            float_to_time(self.duty_time.hour_from))
        duty_to = self.date + ' ' + str(float_to_time(self.duty_time.hour_to))

        duty_from = fields.Datetime.from_string(duty_from)
        duty_to = fields.Datetime.from_string(duty_to)

        self.plan_hours = (duty_to - duty_from).seconds / 3600
        check_in = self.day_log.filtered(lambda log: log.action == 'check_in')
        if check_in:
            signin = min([
                to_naive_user_tz(
                    fields.Datetime.from_string(str(x.action_datetime)),
                    self.env.user) for x in check_in
            ])

            if duty_from > signin:
                self.early_hours = (duty_from - signin).seconds / 3600

            signin = to_naive_utc(signin, self.env.user)
            self.check_in = str(signin)

        check_out = self.day_log.filtered(
            lambda log: log.action == 'check_out')
        if check_out:
            signout = max([
                to_naive_user_tz(
                    fields.Datetime.from_string(str(x.action_datetime)),
                    self.env.user) for x in check_out
            ])

            if signout > duty_to:
                self.late_hours = (signout > duty_to).seconds / 3600

            signout = to_naive_utc(signout, self.env.user)
            self.check_out = str(signout)
        if signin and signout:
            self.worked_hours = (signout - signin).seconds / 3600
Example #4
0
    def _get_calendar_leaves(self):
        calendar_leaves = self.env['resource.calendar.leaves']
        self.calendar_leaves_ids = False

        duty_from = self.date + ' ' + str(
            float_to_time(self.duty_time.hour_from))
        duty_to = self.date + ' ' + str(float_to_time(self.duty_time.hour_to))
        c_date = self.date + ' 00:00:00'

        duty_from = fields.Datetime.from_string(duty_from)
        duty_to = fields.Datetime.from_string(duty_to)

        duty_from = to_naive_user_tz(duty_from, self.env.user)
        duty_to = to_naive_user_tz(duty_to, self.env.user)

        date_leaves = calendar_leaves.search([
            ('resource_id', '=', self.employee_id.resource_id.id),
            ('date_from', '<=', str(duty_from)),
            ('date_to', '>', str(duty_from))
        ])

        date_leaves |= calendar_leaves.search([
            ('resource_id', '=', self.employee_id.resource_id.id),
            ('date_from', '<', str(duty_to)), ('date_to', '>=', str(duty_to))
        ])

        date_leaves |= calendar_leaves.search([
            ('resource_id', '=', self.employee_id.resource_id.id),
            ('date_from', '>=', str(duty_from)),
            ('date_to', '<=', str(duty_to))
        ])
        self.calendar_leaves_ids = date_leaves.ids

        absence_type = None
        excuse_seconds = 0
        return absence_type, excuse_seconds
Example #5
0
    def _new_schedule_days(self,
                           days,
                           day_dt,
                           compute_leaves=False,
                           resource_id=None):
        if not hasattr(self, '_get_schedule_days_iteration_limit'):
            return self._schedule_days_original(days,
                                                day_dt,
                                                compute_leaves=compute_leaves,
                                                resource_id=resource_id)
        # START OF HOOK: Introduce here the iterations limit
        iterations_limit = self._get_schedule_days_iteration_limit() or 100
        # END OF HOOK
        backwards = (days < 0)
        intervals = []
        planned_days, iterations = 0, 0

        day_dt_tz = to_naive_user_tz(day_dt, self.env.user)
        current_datetime = day_dt_tz.replace(hour=0,
                                             minute=0,
                                             second=0,
                                             microsecond=0)

        # HOOK. Use the iterations_limit here
        while planned_days < abs(days) and iterations < iterations_limit:
            working_intervals = self._get_day_work_intervals(
                current_datetime.date(),
                compute_leaves=compute_leaves,
                resource_id=resource_id)
            if not self or working_intervals:
                # no calendar -> no working hours, but day is
                # considered as worked
                planned_days += 1
                intervals += working_intervals
            # get next day
            if backwards:
                current_datetime = self._get_previous_work_day(
                    current_datetime)
            else:
                current_datetime = self._get_next_work_day(current_datetime)
            # avoid infinite loops
            iterations += 1

        return intervals
Example #6
0
    def _get_late(self):
        signin = signout = False
        duty_from = self.date + ' ' + str(
            float_to_time(self.duty_time.hour_from))
        duty_to = self.date + ' ' + str(float_to_time(self.duty_time.hour_to))

        duty_from = fields.Datetime.from_string(duty_from)
        duty_to = fields.Datetime.from_string(duty_to)

        excuse = float_to_time(self.duty_time.calendar_id.excuse)
        excuse = excuse.hour * 3600 + excuse.minute * 60 + excuse.second

        max_late = float_to_time(self.duty_time.calendar_id.max_late)
        max_late = max_late.hour * 3600 + max_late.minute * 60 + max_late.second

        if self.check_in:
            signin = self.check_in
            signin = fields.Datetime.from_string(signin)
            signin = to_naive_user_tz(signin, self.env.user)

        if self.check_out:
            signout = self.check_out
            signout = fields.Datetime.from_string(signout)
            signout = to_naive_user_tz(signout, self.env.user)

        leaves_duration = self._get_leaves_durations()

        absence_type = False
        late = 0.0
        diff_from_duety = 0.0
        leave_signin = False
        leave_signout = False

        #there is at least one leave
        if leaves_duration:
            (leave_signin,
             leave_signout), temp_absence_type = leaves_duration.popitem()
            if not signin or leave_signin < signin:
                signin = leave_signin
            if not signout or leave_signout > signout:
                signout = leave_signout

        if not signin or not signout:
            self.absence_type = 'absent'
            self.total_delay = (duty_to - duty_from).seconds / 3600.0
            self.diff_from_duety = (duty_to - duty_from).seconds / 3600.0
            return

        delta = 0.0
        if signin > duty_from:
            delta += (signin - duty_from).seconds
        if duty_to > signout:
            delta += (duty_to - signout).seconds

        if delta > excuse:
            diff_from_duety = delta - excuse
            if diff_from_duety > max_late:
                self.absence_type = 'absent'
                self.total_delay = (duty_to - duty_from).seconds / 3600.0
                self.diff_from_duety = (duty_to - duty_from).seconds / 3600.0
                return

        if not absence_type:
            if delta == 0:
                absence_type = 'no_delay'
            if delta > 0:
                absence_type = 'delay'

        self.absence_type = absence_type
        self.total_delay = delta / 3600.0
        self.diff_from_duety = diff_from_duety / 3600.0
        return
Example #7
0
    def _get_leaves_durations(self):
        duty_from = self.date + ' ' + str(
            float_to_time(self.duty_time.hour_from))
        duty_to = self.date + ' ' + str(float_to_time(self.duty_time.hour_to))

        duty_from = fields.Datetime.from_string(duty_from)
        duty_to = fields.Datetime.from_string(duty_to)

        date_leaves_dicts = {}
        date_leaves = self.calendar_leaves_ids.filtered(
            lambda x: to_naive_user_tz(
                fields.Datetime.from_string(x.date_from), self.env.user
            ) <= duty_from and to_naive_user_tz(
                fields.Datetime.from_string(x.date_to), self.env.user
            ) >= duty_to)
        date_leaves_dicts.update({(duty_from, duty_to): x.type
                                  for x in date_leaves})

        date_leaves = self.calendar_leaves_ids.filtered(
            lambda x: to_naive_user_tz(
                fields.Datetime.from_string(x.date_from), self.env.user
            ) <= duty_from and to_naive_user_tz(
                fields.Datetime.from_string(x.date_to), self.env.user
            ) < duty_to)
        date_leaves_dicts.update({
            (duty_from,
             to_naive_user_tz(fields.Datetime.from_string(x.date_to),
                              self.env.user)): x.type
            for x in date_leaves
        })

        date_leaves = self.calendar_leaves_ids.filtered(
            lambda x: to_naive_user_tz(
                fields.Datetime.from_string(x.date_from), self.env.user
            ) > duty_from and to_naive_user_tz(
                fields.Datetime.from_string(x.date_to), self.env.user
            ) < duty_to)
        date_leaves_dicts.update({
            (to_naive_user_tz(fields.Datetime.from_string(x.date_from),
                              self.env.user),
             to_naive_user_tz(fields.Datetime.from_string(x.date_to),
                              self.env.user)): x.type
            for x in date_leaves
        })

        date_leaves = self.calendar_leaves_ids.filtered(
            lambda x: to_naive_user_tz(
                fields.Datetime.from_string(x.date_from), self.env.user
            ) > duty_from and to_naive_user_tz(
                fields.Datetime.from_string(x.date_to), self.env.user
            ) > duty_to)
        date_leaves_dicts.update({
            (to_naive_user_tz(fields.Datetime.from_string(x.date_from),
                              self.env.user), duty_to): x.type
            for x in date_leaves
        })

        if date_leaves_dicts:
            #get the leave with the most duration
            keys = date_leaves_dicts.keys()
            keys = {(x[1] - x[0]).seconds: x for x in keys}

            #get the max key using diffrence
            max_key = max(keys.keys())

            #get the max key using keys dict
            max_key = keys[max_key]

            #get the max key using date_leaves_dicts dict
            return {max_key: date_leaves_dicts[max_key]}

        return {}
Example #8
0
    def _get_attendance_log(self):
        self.day_log = False
        att_log = self.env['hr.attendance.log']
        start_date = self.date + ' 00:00:00'
        end_date = self.date + ' 23:59:59'
        attendance_log_ids = att_log.search([
            ('employee_id', '=', self.employee_id.id),
            ('action_datetime', '>=', start_date),
            ('action_datetime', '<=', end_date),
            ('state', 'in', ['approved', 'fetched'])
        ])

        attendances = self._get_attendances(self.employee_id, self.date)
        attendances_recs = self.search([('date', '=', self.date),
                                        ('duty_time', 'in', attendances.ids)])

        morning_duty = attendances_recs.filtered(
            lambda x: x.duty_time.shift == 'morning')
        evening_duty = attendances_recs.filtered(
            lambda x: x.duty_time.shift == 'evening')

        if morning_duty:
            morning_duty = morning_duty[0] or False
            shift_from = self.date + ' ' + str(
                float_to_time(morning_duty.duty_time.hour_from))
            shift_to = self.date + ' ' + str(
                float_to_time(morning_duty.duty_time.hour_to))

            shift_from = fields.Datetime.from_string(shift_from)
            shift_to = fields.Datetime.from_string(shift_to)

            if not evening_duty:
                morning_signs = attendance_log_ids
            if evening_duty:
                evening_shift_from = self.date + ' ' + str(
                    float_to_time(evening_duty[0].duty_time.hour_from))
                evening_shift_from = fields.Datetime.from_string(
                    evening_shift_from)

                morning_signs = attendance_log_ids.filtered(
                    lambda x: to_naive_user_tz(
                        fields.Datetime.from_string(x.action_datetime), self.
                        env.user) <= shift_to or (to_naive_user_tz(
                            fields.Datetime.from_string(x.action_datetime),
                            self.env.user) < evening_shift_from and x.action ==
                                                  'check_out'))
            morning_signs.write({'attendance_id': morning_duty.id})
        if evening_duty:
            evening_duty = evening_duty[0] or False
            shift_from = self.date + ' ' + str(
                float_to_time(evening_duty.duty_time.hour_from))
            shift_to = self.date + ' ' + str(
                float_to_time(evening_duty.duty_time.hour_to))

            shift_from = fields.Datetime.from_string(shift_from)
            shift_to = fields.Datetime.from_string(shift_to)

            if not morning_duty:
                evening_signs = attendance_log_ids

            if morning_duty:
                morning_shift_to = self.date + ' ' + str(
                    float_to_time(morning_duty[0].duty_time.hour_to))
                morning_shift_to = fields.Datetime.from_string(
                    morning_shift_to)

                evening_signs = attendance_log_ids.filtered(
                    lambda x: to_naive_user_tz(
                        fields.Datetime.from_string(x.action_datetime), self.
                        env.user) >= shift_from or (to_naive_user_tz(
                            fields.Datetime.from_string(x.action_datetime),
                            self.env.user) > morning_shift_to and x.action ==
                                                    'chech_in'))
            evening_signs.write({'attendance_id': evening_duty.id})

        return True
Example #9
0
 def test_calendar_days_scheduling_timezone(self):
     self.env.user.tz = 'US/Alaska'
     res = self.calendar.plan_days(5, to_naive_utc(Datetime.from_string('2013-02-12 09:08:07'), self.env.user))
     self.assertEqual(to_naive_user_tz(res, self.env.user).date(), Datetime.from_string('2013-02-26 00:00:00').date())
Example #10
0
    def get_worked_within_after_hours(self, start_datetime, end_datetime, resource_id,
                                      min_tolerance_minutes=0):
        """
        Obtains the worked hours within and after the calendar for the related resource
        :param start_datetime:
        :param end_datetime:
        :param resource_id:
        :param min_tolerance_minutes:
        :return:
        """
        # TODO
        # * filter by public holidays
        # * use logger instead print

        # For an alternative calculation, see "_interval_remove_leaves" method

        work_intervals = []
        # The intervals are separated by days. For the current calculation,
        #  we prefer a single list
        _logger.info(
            'Within and after hours calculation for intervals between %s to %s, calendar_id=%d and resource_id=%d:'
            % (start_datetime, end_datetime, self.id, resource_id))
        i = 1
        for interval in self._iter_work_intervals(
                start_dt=start_datetime,
                end_dt=end_datetime,
                resource_id=resource_id):
            # data = [i]; data.extend(interval)
            k = 1
            for inter in interval:
                work_intervals.append(inter[0:2])
                _logger.info('%d.%d) from %s to %s' % (i, k, inter[0], inter[1]))
                k += 1
            i += 1

        # The current interval list does not take into account the public holidays
        # We're supress those intervals whose start datetime matches a public holiday day
        # UTC/TZ problem: datetime_from and datetime_and are expressed taking into account user TZ, because
        #  is_public_holiday uses attendance intervals un UTC.
        # E.g. with Europe/Madrid TZ, the public holiday 2019-03-19 (winter) actually starts
        #      in order to make comparisons with hr.attendance intervals we make check_in UTC date to user's TZ
        #      An attendance starting in 2019-03-18 23:30:00 UTC (or 2019-03-19 00:30:00 UTC+1) is converted to
        #      "2019-03-19 00:30:00 UTC" and is_public_holiday(), which compares only dates, is able to return True
        # As we said, We use the user TZ for this (manager users should be in the same TZ of the employees)
        employee_id = self.env['hr.employee'].search([('resource_id', '=', resource_id)]).id
        HolidaysPublic = self.env['hr.holidays.public']
        work_intervals = list(filter(
            lambda x: not HolidaysPublic.is_public_holiday(to_naive_user_tz(x[0], self.env.user),
                                                           employee_id=employee_id),
            work_intervals))

        worked_hours_within_calendar = worked_hours_after_calendar = 0

        # On every iteration, we compare the current work_interval with the last "initial" datetime registered
        #  (that will be the last "end" datetime or the attendance check in
        # Thus, the last iteration will confront the last "end" datetime for the work_interval (or the check in) with
        #  the check out
        dt_start = start_datetime
        min_tolerance_secs = 60 * min_tolerance_minutes
        for work_interval in work_intervals:
            work_dt_start = work_interval[0]
            work_dt_end = work_interval[1]
            offset_secs = (work_dt_start - dt_start).total_seconds()
            worked_hours_after_calendar += offset_secs / 3600 if offset_secs >= min_tolerance_secs else 0.0
            worked_hours_within_calendar += offset_secs / 3600 if offset_secs < min_tolerance_secs else 0.0
            worked_hours_within_calendar += (work_dt_end - work_dt_start).total_seconds() / 3600
            dt_start = work_dt_end
        if dt_start < end_datetime:
            # FIXME this will be the last (or unique) "empty" interval.
            #       But, if it isn't close to the next work interval and is lonely,
            #       should we always consider as after time?
            #       We don't know the work intervals outside (start_datetime, end_datetime)
            offset_secs = (end_datetime - dt_start).total_seconds()
            worked_hours_after_calendar += offset_secs / 3600 if offset_secs >= min_tolerance_secs else 0.0
            worked_hours_within_calendar += offset_secs / 3600 if offset_secs < min_tolerance_secs else 0.0

        """
        worked_hours_within_calendar = (end_datetime - start_datetime).total_seconds() / 3600
        worked_hours_after_calendar = 0
        """
        _logger.info('Obtained (%f, %f)' % (worked_hours_within_calendar, worked_hours_after_calendar))
        return worked_hours_within_calendar, worked_hours_after_calendar