diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py index 15c39e25792..ea84de93764 100644 --- a/addons/resource/models/resource.py +++ b/addons/resource/models/resource.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from collections import defaultdict import math from datetime import datetime, time, timedelta from dateutil.relativedelta import relativedelta @@ -14,12 +15,15 @@ from odoo.addons.base.models.res_partner import _tz_get from odoo.exceptions import ValidationError from odoo.osv import expression from odoo.tools.float_utils import float_round -from odoo.tools import date_utils + +from odoo.tools import date_utils, float_utils # Default hour per day value. The one should # only be used when the one from the calendar # is not available. HOURS_PER_DAY = 8 +# This will generate 16th of days +ROUNDING_FACTOR = 16 def make_aware(dt): @@ -396,6 +400,45 @@ class ResourceCalendar(models.Model): return (self._attendance_intervals(start_dt, end_dt, resource) - self._leave_intervals(start_dt, end_dt, resource, domain)) + # -------------------------------------------------- + # Private Methods / Helpers + # -------------------------------------------------- + + 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, resource=None): + """ + @return dict with hours of attendance in each day between `from_datetime` and `to_datetime` + """ + self.ensure_one() + # 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 = self._attendance_intervals(from_full, to_full, resource=resource) + day_total = defaultdict(float) + for start, stop, meta in intervals: + day_total[start.date()] += (stop - start).total_seconds() / 3600 + return day_total + # -------------------------------------------------- # External API # -------------------------------------------------- @@ -426,6 +469,32 @@ class ResourceCalendar(models.Model): for start, stop, meta in intervals ) + def get_work_duration_data(self, from_datetime, to_datetime, compute_leaves=True, domain=None): + """ + Get the working duration (in days and hours) for a given period, only + based on the current calendar. This method does not use resource to + compute it. + + `domain` is used in order to recognise the leaves to take, + None means default value ('time_type', '=', 'leave') + + Returns a dict {'days': n, 'hours': h} containing the + quantity of working time expressed as days and as hours. + """ + # naive datetimes are made explicit in UTC + from_datetime, dummy = make_aware(from_datetime) + to_datetime, dummy = make_aware(to_datetime) + + day_total = self._get_day_total(from_datetime, to_datetime) + + # actual hours per day + if compute_leaves: + intervals = self._work_intervals(from_datetime, to_datetime, domain=domain) + else: + intervals = self._attendance_intervals(from_datetime, to_datetime) + + return self._get_days_data(intervals, day_total) + def plan_hours(self, hours, day_dt, compute_leaves=False, domain=None, resource=None): """ `compute_leaves` controls whether or not this method is taking into diff --git a/addons/resource/models/resource_mixin.py b/addons/resource/models/resource_mixin.py index 46caea0c24b..1358ffd1f7a 100644 --- a/addons/resource/models/resource_mixin.py +++ b/addons/resource/models/resource_mixin.py @@ -2,14 +2,9 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from collections import defaultdict -from datetime import timedelta from pytz import utc from odoo import api, fields, models -from odoo.tools import float_utils - -# This will generate 16th of days -ROUNDING_FACTOR = 16 def timezone_datetime(time): @@ -58,41 +53,6 @@ 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 _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 @@ -111,7 +71,7 @@ class ResourceMixin(models.AbstractModel): from_datetime = timezone_datetime(from_datetime) to_datetime = timezone_datetime(to_datetime) - day_total = self._get_day_total(from_datetime, to_datetime, calendar, resource) + day_total = calendar._get_day_total(from_datetime, to_datetime, resource) # actual hours per day if compute_leaves: @@ -119,7 +79,7 @@ class ResourceMixin(models.AbstractModel): else: intervals = calendar._attendance_intervals(from_datetime, to_datetime, resource) - return self._get_days_data(intervals, day_total) + return calendar._get_days_data(intervals, day_total) def _get_leave_days_data(self, from_datetime, to_datetime, calendar=None, domain=None): """ @@ -139,14 +99,13 @@ class ResourceMixin(models.AbstractModel): from_datetime = timezone_datetime(from_datetime) to_datetime = timezone_datetime(to_datetime) - day_total = self._get_day_total(from_datetime, to_datetime, calendar, resource) + day_total = calendar._get_day_total(from_datetime, to_datetime, 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) - return self._get_days_data(attendances & leaves, day_total) - + return calendar._get_days_data(attendances & leaves, day_total) def list_work_time_per_day(self, from_datetime, to_datetime, calendar=None, domain=None): """