# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from collections import defaultdict from dateutil.relativedelta import relativedelta from pytz import timezone from odoo import api, fields, models, _ from odoo.addons.base.models.res_partner import _tz_get from .utils import timezone_datetime, make_aware, Intervals class ResourceResource(models.Model): _name = "resource.resource" _description = "Resources" _order = "name" @api.model def default_get(self, fields): res = super().default_get(fields) if not res.get('calendar_id') and res.get('company_id'): company = self.env['res.company'].browse(res['company_id']) res['calendar_id'] = company.resource_calendar_id.id return res name = fields.Char(required=True) active = fields.Boolean( 'Active', default=True, help="If the active field is set to False, it will allow you to hide the resource record without removing it.") company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company) resource_type = fields.Selection([ ('user', 'Human'), ('material', 'Material')], string='Type', default='user', required=True) user_id = fields.Many2one('res.users', string='User', help='Related user name for the resource to manage its access.') time_efficiency = fields.Float( 'Efficiency Factor', default=100, required=True, help="This field is used to calculate the expected duration of a work order at this work center. For example, if a work order takes one hour and the efficiency factor is 100%, then the expected duration will be one hour. If the efficiency factor is 200%, however the expected duration will be 30 minutes.") calendar_id = fields.Many2one( "resource.calendar", string='Working Time', default=lambda self: self.env.company.resource_calendar_id, domain="[('company_id', '=', company_id)]") tz = fields.Selection( _tz_get, string='Timezone', required=True, default=lambda self: self._context.get('tz') or self.env.user.tz or 'UTC') _sql_constraints = [ ('check_time_efficiency', 'CHECK(time_efficiency>0)', 'Time efficiency must be strictly positive'), ] @api.model_create_multi def create(self, vals_list): for values in vals_list: if values.get('company_id') and not values.get('calendar_id'): values['calendar_id'] = self.env['res.company'].browse(values['company_id']).resource_calendar_id.id if not values.get('tz'): # retrieve timezone on user or calendar tz = (self.env['res.users'].browse(values.get('user_id')).tz or self.env['resource.calendar'].browse(values.get('calendar_id')).tz) if tz: values['tz'] = tz return super().create(vals_list) @api.returns('self', lambda value: value.id) def copy(self, default=None): self.ensure_one() if default is None: default = {} if not default.get('name'): default['name'] = _('%s (copy)', self.name) return super().copy(default) def write(self, values): if self.env.context.get('check_idempotence') and len(self) == 1: values = { fname: value for fname, value in values.items() if self._fields[fname].convert_to_write(self[fname], self) != value } if not values: return True return super().write(values) @api.onchange('company_id') def _onchange_company_id(self): if self.company_id: self.calendar_id = self.company_id.resource_calendar_id.id @api.onchange('user_id') def _onchange_user_id(self): if self.user_id: self.tz = self.user_id.tz def _get_work_interval(self, start, end): # Deprecated method. Use `_adjust_to_calendar` instead return self._adjust_to_calendar(start, end) def _adjust_to_calendar(self, start, end, compute_leaves=True): """Adjust the given start and end datetimes to the closest effective hours encoded in the resource calendar. Only attendances in the same day as `start` and `end` are considered (respectively). If no attendance is found during that day, the closest hour is None. e.g. simplified example: given two attendances: 8am-1pm and 2pm-5pm, given start=9am and end=6pm resource._adjust_to_calendar(start, end) >>> {resource: (8am, 5pm)} :return: Closest matching start and end of working periods for each resource :rtype: dict(resource, tuple(datetime | None, datetime | None)) """ start, revert_start_tz = make_aware(start) end, revert_end_tz = make_aware(end) result = {} for resource in self: resource_tz = timezone(resource.tz) start, end = start.astimezone(resource_tz), end.astimezone(resource_tz) search_range = [ start + relativedelta(hour=0, minute=0, second=0), end + relativedelta(days=1, hour=0, minute=0, second=0), ] calendar = resource.calendar_id or resource.company_id.resource_calendar_id or self.env.company.resource_calendar_id calendar_start = calendar._get_closest_work_time(start, resource=resource, search_range=search_range, compute_leaves=compute_leaves) search_range[0] = start calendar_end = calendar._get_closest_work_time(max(start, end), match_end=True, resource=resource, search_range=search_range, compute_leaves=compute_leaves) result[resource] = ( calendar_start and revert_start_tz(calendar_start), calendar_end and revert_end_tz(calendar_end), ) return result def _get_unavailable_intervals(self, start, end): """ Compute the intervals during which employee is unavailable with hour granularity between start and end Note: this method is used in enterprise (forecast and planning) """ start_datetime = timezone_datetime(start) end_datetime = timezone_datetime(end) resource_mapping = {} calendar_mapping = defaultdict(lambda: self.env['resource.resource']) for resource in self: calendar_mapping[resource.calendar_id or resource.company_id.resource_calendar_id] |= resource for calendar, resources in calendar_mapping.items(): resources_unavailable_intervals = calendar._unavailable_intervals_batch(start_datetime, end_datetime, resources, tz=timezone(calendar.tz)) resource_mapping.update(resources_unavailable_intervals) return resource_mapping def _get_calendars_validity_within_period(self, start, end, default_company=None): """ Gets 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. Here the validity interval for each calendar is the whole interval but it's meant to be overriden in further modules handling resource's employee contracts. """ assert start.tzinfo and end.tzinfo resource_calendars_within_period = defaultdict(lambda: defaultdict(Intervals)) # keys are [resource id:integer][calendar:self.env['resource.calendar']] default_calendar = default_company and default_company.resource_calendar_id or self.env.company.resource_calendar_id if not self: # if no resource, add the company resource calendar. resource_calendars_within_period[False][default_calendar] = Intervals([(start, end, self.env['resource.calendar.attendance'])]) for resource in self: calendar = resource.calendar_id or resource.company_id.resource_calendar_id or default_calendar resource_calendars_within_period[resource.id][calendar] = Intervals([(start, end, self.env['resource.calendar.attendance'])]) return resource_calendars_within_period def _get_valid_work_intervals(self, start, end, calendars=None): """ Gets the valid work intervals of the resource following their calendars between ``start`` and ``end`` This methods handle the eventuality of a resource having multiple resource calendars, see _get_calendars_validity_within_period method for further explanation. """ assert start.tzinfo and end.tzinfo resource_calendar_validity_intervals = {} calendar_resources = defaultdict(lambda: self.env['resource.resource']) resource_work_intervals = defaultdict(Intervals) calendar_work_intervals = dict() resource_calendar_validity_intervals = self.sudo()._get_calendars_validity_within_period(start, end) for resource in self: # For each resource, retrieve its calendar and their validity intervals for calendar in resource_calendar_validity_intervals[resource.id]: calendar_resources[calendar] |= resource for calendar in (calendars or []): calendar_resources[calendar] |= self.env['resource.resource'] for calendar, resources in calendar_resources.items(): # For each calendar used by the resources, retrieve the work intervals for every resources using it work_intervals_batch = calendar._work_intervals_batch(start, end, resources=resources) for resource in resources: # Make the conjunction between work intervals and calendar validity resource_work_intervals[resource.id] |= work_intervals_batch[resource.id] & resource_calendar_validity_intervals[resource.id][calendar] calendar_work_intervals[calendar.id] = work_intervals_batch[False] return resource_work_intervals, calendar_work_intervals