From 5a9b8a41b692061548af2cb543171c503e802ade Mon Sep 17 00:00:00 2001 From: lul-odoo Date: Wed, 14 Nov 2018 13:55:01 +0100 Subject: [PATCH] [IMP] resource: Add `domain` parameter to compute attendace intervals Purpose ======= More flexibility is useful when using the _attendance_intervals method (resource.calendar) in other modules when inheriting resource.calendar Specification ============= The commit adds a `domain` parameter to the _attendance_intervals method in resource.calendar, allowing to better control attendances from which to compute intervals. + small refactoring: extraction of duplicated code in method --- addons/resource/models/resource.py | 12 ++- addons/resource/models/resource_mixin.py | 98 ++++++++++++------------ 2 files changed, 60 insertions(+), 50 deletions(-) diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py index 08d6b9e73e9..481cb9b57df 100644 --- a/addons/resource/models/resource.py +++ b/addons/resource/models/resource.py @@ -197,13 +197,20 @@ class ResourceCalendar(models.Model): # -------------------------------------------------- # Computation API # -------------------------------------------------- - def _attendance_intervals(self, start_dt, end_dt, resource=None): + def _attendance_intervals(self, start_dt, end_dt, resource=None, domain=None): """ Return the attendance intervals in the given datetime range. The returned intervals are expressed in the resource's timezone. """ assert start_dt.tzinfo and end_dt.tzinfo combine = datetime.combine + resource_ids = [resource.id, False] if resource else [False] + domain = domain if domain is not None else [] + domain = domain + [ + ('calendar_id', '=', self.id), + ('resource_id', 'in', resource_ids), + ] + # express all dates and times in the resource's timezone tz = timezone((resource or self).tz) start_dt = start_dt.astimezone(tz) @@ -211,7 +218,7 @@ class ResourceCalendar(models.Model): # for each attendance spec, generate the intervals in the date range result = [] - for attendance in self.attendance_ids: + for attendance in self.env['resource.calendar.attendance'].search(domain): start = start_dt.date() if attendance.date_from: start = max(start, attendance.date_from) @@ -404,6 +411,7 @@ class ResourceCalendarAttendance(models.Model): hour_to = fields.Float(string='Work to', required=True) calendar_id = fields.Many2one("resource.calendar", string="Resource's Calendar", required=True, ondelete='cascade') day_period = fields.Selection([('morning', 'Morning'), ('afternoon', 'Afternoon')], required=True, default='morning') + resource_id = fields.Many2one('resource.resource', 'Resource') @api.onchange('hour_from', 'hour_to') def _onchange_hours(self): diff --git a/addons/resource/models/resource_mixin.py b/addons/resource/models/resource_mixin.py index a55911398af..4108146149d 100644 --- a/addons/resource/models/resource_mixin.py +++ b/addons/resource/models/resource_mixin.py @@ -53,6 +53,47 @@ class ResourceMixin(models.AbstractModel): default['resource_calendar_id'] = resource.calendar_id.id return super(ResourceMixin, self).copy_data(default) + def _get_days_data(self, intervals, day_total): + """ + helper function to compute duration of `intervals` + expressed in days and hours. + `day_total` is a dict {date: n_hours} with the number of hours for each day. + """ + day_hours = defaultdict(float) + for start, stop, meta in intervals: + day_hours[start.date()] += (stop - start).total_seconds() / 3600 + + # compute number of days as quarters + days = sum( + float_utils.round(ROUNDING_FACTOR * day_hours[day] / day_total[day]) / ROUNDING_FACTOR + for day in day_hours + ) + return { + 'days': days, + 'hours': sum(day_hours.values()), + } + + def _get_day_total(self, from_datetime, to_datetime, calendar, resource): + """ + @return dict with hours of attendance in each day between `from_datetime` and `to_datetime` + """ + + # total hours per day: retrieve attendances with one extra day margin, + # in order to compute the total hours on the first and last days + from_full = from_datetime - timedelta(days=1) + to_full = to_datetime + timedelta(days=1) + intervals = calendar._attendance_intervals(from_full, to_full, resource) + day_total = defaultdict(float) + for start, stop, meta in intervals: + day_total[start.date()] += (stop - start).total_seconds() / 3600 + return day_total + + def _timezone_datetime(self, time): + if not time.tzinfo: + time = time.replace(tzinfo=utc) + return time + + def get_work_days_data(self, from_datetime, to_datetime, compute_leaves=True, calendar=None, domain=None): """ By default the resource calendar is used, but it can be @@ -68,38 +109,18 @@ class ResourceMixin(models.AbstractModel): calendar = calendar or self.resource_calendar_id # naive datetimes are made explicit in UTC - if not from_datetime.tzinfo: - from_datetime = from_datetime.replace(tzinfo=utc) - if not to_datetime.tzinfo: - to_datetime = to_datetime.replace(tzinfo=utc) + from_datetime = self._timezone_datetime(from_datetime) + to_datetime = self._timezone_datetime(to_datetime) - # total hours per day: retrieve attendances with one extra day margin, - # in order to compute the total hours on the first and last days - from_full = from_datetime - timedelta(days=1) - to_full = to_datetime + timedelta(days=1) - intervals = calendar._attendance_intervals(from_full, to_full, resource) - day_total = defaultdict(float) - for start, stop, meta in intervals: - day_total[start.date()] += (stop - start).total_seconds() / 3600 + day_total = self._get_day_total(from_datetime, to_datetime, calendar, resource) # actual hours per day if compute_leaves: intervals = calendar._work_intervals(from_datetime, to_datetime, resource, domain) else: intervals = calendar._attendance_intervals(from_datetime, to_datetime, resource) - day_hours = defaultdict(float) - for start, stop, meta in intervals: - day_hours[start.date()] += (stop - start).total_seconds() / 3600 - # compute number of days as quarters - days = sum( - float_utils.round(ROUNDING_FACTOR * day_hours[day] / day_total[day]) / ROUNDING_FACTOR - for day in day_hours - ) - return { - 'days': days, - 'hours': sum(day_hours.values()), - } + return self._get_days_data(intervals, day_total) def get_leave_days_data(self, from_datetime, to_datetime, calendar=None, domain=None): """ @@ -116,36 +137,17 @@ class ResourceMixin(models.AbstractModel): calendar = calendar or self.resource_calendar_id # naive datetimes are made explicit in UTC - if not from_datetime.tzinfo: - from_datetime = from_datetime.replace(tzinfo=utc) - if not to_datetime.tzinfo: - to_datetime = to_datetime.replace(tzinfo=utc) + from_datetime = self._timezone_datetime(from_datetime) + to_datetime = self._timezone_datetime(to_datetime) - # total hours per day: retrieve attendances with one extra day margin, - # in order to compute the total hours on the first and last days - from_full = from_datetime - timedelta(days=1) - to_full = to_datetime + timedelta(days=1) - intervals = calendar._attendance_intervals(from_full, to_full, resource) - day_total = defaultdict(float) - for start, stop, meta in intervals: - day_total[start.date()] += (stop - start).total_seconds() / 3600 + day_total = self._get_day_total(from_datetime, to_datetime, calendar, resource) # compute actual hours per day attendances = calendar._attendance_intervals(from_datetime, to_datetime, resource) leaves = calendar._leave_intervals(from_datetime, to_datetime, resource, domain) - day_hours = defaultdict(float) - for start, stop, meta in (attendances & leaves): - day_hours[start.date()] += (stop - start).total_seconds() / 3600 - # compute number of days as quarters - days = sum( - float_utils.round(ROUNDING_FACTOR * day_hours[day] / day_total[day]) / ROUNDING_FACTOR - for day in day_hours - ) - return { - 'days': days, - 'hours': sum(day_hours.values()), - } + return self._get_days_data(attendances & leaves, day_total) + def list_work_time_per_day(self, from_datetime, to_datetime, calendar=None, domain=None): """