# -*- coding: utf-8 -*- # 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): if not time.tzinfo: time = time.replace(tzinfo=utc) return time class ResourceMixin(models.AbstractModel): _name = "resource.mixin" _description = 'Resource Mixin' resource_id = fields.Many2one( 'resource.resource', 'Resource', auto_join=True, index=True, ondelete='restrict', required=True) company_id = fields.Many2one( 'res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get(), index=True, related='resource_id.company_id', store=True, readonly=False) resource_calendar_id = fields.Many2one( 'resource.calendar', 'Working Hours', default=lambda self: self.env['res.company']._company_default_get().resource_calendar_id, index=True, related='resource_id.calendar_id', store=True, readonly=False) tz = fields.Selection( string='Timezone', related='resource_id.tz', readonly=False, help="This field is used in order to define in which timezone the resources will work.") @api.model def create(self, values): if not values.get('resource_id'): resource_vals = {'name': values.get(self._rec_name)} tz = (values.pop('tz', False) or self.env['resource.calendar'].browse(values.get('resource_calendar_id')).tz) if tz: resource_vals['tz'] = tz resource = self.env['resource.resource'].create(resource_vals) values['resource_id'] = resource.id return super(ResourceMixin, self).create(values) @api.multi def copy_data(self, default=None): if default is None: default = {} resource = self.resource_id.copy() default['resource_id'] = resource.id default['company_id'] = resource.company_id.id 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 changed using the `calendar` argument. `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. """ resource = self.resource_id calendar = calendar or self.resource_calendar_id # naive datetimes are made explicit in UTC from_datetime = timezone_datetime(from_datetime) to_datetime = timezone_datetime(to_datetime) 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) return self._get_days_data(intervals, day_total) def _get_leave_days_data(self, from_datetime, to_datetime, calendar=None, domain=None): """ By default the resource calendar is used, but it can be changed using the `calendar` argument. `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 number of leaves expressed as days and as hours. """ resource = self.resource_id calendar = calendar or self.resource_calendar_id # naive datetimes are made explicit in UTC from_datetime = timezone_datetime(from_datetime) to_datetime = timezone_datetime(to_datetime) 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) return self._get_days_data(attendances & leaves, day_total) def list_work_time_per_day(self, from_datetime, to_datetime, calendar=None, domain=None): """ By default the resource calendar is used, but it can be changed using the `calendar` argument. `domain` is used in order to recognise the leaves to take, None means default value ('time_type', '=', 'leave') Returns a list of tuples (day, hours) for each day containing at least an attendance. """ resource = self.resource_id 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) intervals = calendar._work_intervals(from_datetime, to_datetime, resource, domain) result = defaultdict(float) for start, stop, meta in intervals: result[start.date()] += (stop - start).total_seconds() / 3600 return sorted(result.items()) def list_leaves(self, from_datetime, to_datetime, calendar=None, domain=None): """ By default the resource calendar is used, but it can be changed using the `calendar` argument. `domain` is used in order to recognise the leaves to take, None means default value ('time_type', '=', 'leave') Returns a list of tuples (day, hours, resource.calendar.leaves) for each leave in the calendar. """ resource = self.resource_id 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) attendances = calendar._attendance_intervals(from_datetime, to_datetime, resource) leaves = calendar._leave_intervals(from_datetime, to_datetime, resource, domain) result = [] for start, stop, leave in (leaves & attendances): hours = (stop - start).total_seconds() / 3600 result.append((start.date(), hours, leave)) return result