From 7bce5f3f95429a4d4ba034a66c350ee2a5868567 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Mon, 19 Dec 2022 09:14:42 +0100 Subject: [PATCH] [MOV] resource: Split files according to coding guidelines --- addons/hr/models/resource.py | 3 - addons/hr/tests/test_resource.py | 7 +- .../hr_contract/models/resource_resource.py | 2 +- addons/hr_contract/tests/test_resource.py | 2 +- addons/hr_holidays/models/hr_employee.py | 2 +- addons/hr_holidays/models/hr_leave.py | 2 +- .../hr_holidays/models/hr_leave_allocation.py | 2 +- addons/hr_holidays/models/hr_leave_type.py | 2 +- .../models/hr_contract.py | 6 +- addons/mrp/models/mrp_workcenter.py | 2 +- addons/resource/__manifest__.py | 6 +- addons/resource/models/__init__.py | 7 +- .../{resource.py => resource_calendar.py} | 470 +----------------- .../models/resource_calendar_attendance.py | 89 ++++ .../models/resource_calendar_leaves.py | 70 +++ addons/resource/models/resource_mixin.py | 10 +- addons/resource/models/resource_resource.py | 195 ++++++++ addons/resource/models/utils.py | 137 +++++ addons/resource/views/menuitems.xml | 18 + .../resource_calendar_attendance_views.xml | 45 ++ .../views/resource_calendar_leaves_views.xml | 110 ++++ .../views/resource_calendar_views.xml | 84 ++++ .../views/resource_resource_views.xml | 92 ++++ addons/test_resource/tests/test_resource.py | 2 +- .../models/event_sponsor.py | 2 +- 25 files changed, 877 insertions(+), 490 deletions(-) rename addons/resource/models/{resource.py => resource_calendar.py} (61%) create mode 100644 addons/resource/models/resource_calendar_attendance.py create mode 100644 addons/resource/models/resource_calendar_leaves.py create mode 100644 addons/resource/models/resource_resource.py create mode 100644 addons/resource/views/menuitems.xml create mode 100644 addons/resource/views/resource_calendar_attendance_views.xml create mode 100644 addons/resource/views/resource_calendar_leaves_views.xml create mode 100644 addons/resource/views/resource_calendar_views.xml create mode 100644 addons/resource/views/resource_resource_views.xml diff --git a/addons/hr/models/resource.py b/addons/hr/models/resource.py index 64b5f52db31..05af1c176cf 100644 --- a/addons/hr/models/resource.py +++ b/addons/hr/models/resource.py @@ -1,10 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from pytz import utc, timezone -from datetime import datetime from odoo import fields, models -from odoo.addons.resource.models.resource import Intervals class ResourceResource(models.Model): diff --git a/addons/hr/tests/test_resource.py b/addons/hr/tests/test_resource.py index 19074b3f75b..82fabb58554 100644 --- a/addons/hr/tests/test_resource.py +++ b/addons/hr/tests/test_resource.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from datetime import datetime -from pytz import timezone, utc -from odoo.addons.resource.models.resource import Intervals, sum_intervals +from datetime import datetime +from pytz import utc + +from odoo.addons.resource.models.utils import Intervals from .common import TestHrCommon diff --git a/addons/hr_contract/models/resource_resource.py b/addons/hr_contract/models/resource_resource.py index 953106496aa..c2c9f9fb2c9 100644 --- a/addons/hr_contract/models/resource_resource.py +++ b/addons/hr_contract/models/resource_resource.py @@ -5,7 +5,7 @@ from datetime import datetime from pytz import timezone from odoo import models -from odoo.addons.resource.models.resource import Intervals +from odoo.addons.resource.models.utils import Intervals class ResourceResource(models.Model): _inherit = 'resource.resource' diff --git a/addons/hr_contract/tests/test_resource.py b/addons/hr_contract/tests/test_resource.py index d81152edb3c..3549124aeed 100644 --- a/addons/hr_contract/tests/test_resource.py +++ b/addons/hr_contract/tests/test_resource.py @@ -3,7 +3,7 @@ from datetime import date, datetime from pytz import utc, timezone -from odoo.addons.resource.models.resource import Intervals, sum_intervals +from odoo.addons.resource.models.utils import Intervals, sum_intervals from odoo.fields import Date from .common import TestContractCommon diff --git a/addons/hr_holidays/models/hr_employee.py b/addons/hr_holidays/models/hr_employee.py index 0e2e065c419..16a318988a2 100644 --- a/addons/hr_holidays/models/hr_employee.py +++ b/addons/hr_holidays/models/hr_employee.py @@ -6,7 +6,7 @@ from dateutil.relativedelta import relativedelta from odoo import _, api, fields, models from odoo.tools.float_utils import float_round -from odoo.addons.resource.models.resource import HOURS_PER_DAY +from odoo.addons.resource.models.utils import HOURS_PER_DAY class HrEmployeeBase(models.AbstractModel): diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 625fc331886..0d63291c8ef 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -14,7 +14,7 @@ from odoo.tools import date_utils from odoo import api, Command, fields, models, tools from odoo.addons.base.models.res_partner import _tz_get -from odoo.addons.resource.models.resource import float_to_time, HOURS_PER_DAY +from odoo.addons.resource.models.utils import float_to_time, HOURS_PER_DAY from odoo.exceptions import AccessError, UserError, ValidationError from odoo.tools import float_compare, format_date from odoo.tools.float_utils import float_round diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index ac1c5cf4ad0..16731bc551a 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -10,7 +10,7 @@ from datetime import datetime, time from dateutil.relativedelta import relativedelta from odoo import api, fields, models -from odoo.addons.resource.models.resource import HOURS_PER_DAY +from odoo.addons.resource.models.utils import HOURS_PER_DAY from odoo.addons.hr_holidays.models.hr_leave import get_employee_from_context from odoo.exceptions import AccessError, UserError, ValidationError from odoo.tools.translate import _ diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py index c764b8a6a0e..437df216546 100644 --- a/addons/hr_holidays/models/hr_leave_type.py +++ b/addons/hr_holidays/models/hr_leave_type.py @@ -14,7 +14,7 @@ from odoo.osv import expression from odoo.tools import format_date from odoo.tools.translate import _ from odoo.tools.float_utils import float_round -from odoo.addons.resource.models.resource import Intervals +from odoo.addons.resource.models.utils import Intervals _logger = logging.getLogger(__name__) diff --git a/addons/hr_work_entry_contract/models/hr_contract.py b/addons/hr_work_entry_contract/models/hr_contract.py index 7b620c24e23..ac317ea7158 100644 --- a/addons/hr_work_entry_contract/models/hr_contract.py +++ b/addons/hr_work_entry_contract/models/hr_contract.py @@ -2,14 +2,14 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import itertools -import pytz - from collections import defaultdict from datetime import datetime, date +import pytz + from dateutil.relativedelta import relativedelta from odoo import api, fields, models, _ -from odoo.addons.resource.models.resource import datetime_to_string, string_to_datetime, Intervals +from odoo.addons.resource.models.utils import string_to_datetime, Intervals from odoo.osv import expression from odoo.exceptions import UserError diff --git a/addons/mrp/models/mrp_workcenter.py b/addons/mrp/models/mrp_workcenter.py index e2e6f6b21ad..d402dee5eef 100644 --- a/addons/mrp/models/mrp_workcenter.py +++ b/addons/mrp/models/mrp_workcenter.py @@ -9,7 +9,7 @@ from random import randint from odoo import api, exceptions, fields, models, _ from odoo.exceptions import UserError, ValidationError -from odoo.addons.resource.models.resource import make_aware, Intervals +from odoo.addons.resource.models.utils import make_aware, Intervals from odoo.tools.float_utils import float_compare diff --git a/addons/resource/__manifest__.py b/addons/resource/__manifest__.py index 2d310c58a7a..1cb2807e6e7 100644 --- a/addons/resource/__manifest__.py +++ b/addons/resource/__manifest__.py @@ -18,7 +18,11 @@ associated to every resource. It also manages the leaves of every resource. 'data/resource_data.xml', 'security/ir.model.access.csv', 'security/resource_security.xml', - 'views/resource_views.xml', + 'views/resource_resource_views.xml', + 'views/resource_calendar_leaves_views.xml', + 'views/resource_calendar_attendance_views.xml', + 'views/resource_calendar_views.xml', + 'views/menuitems.xml', ], 'assets': { 'web.assets_backend': [ diff --git a/addons/resource/models/__init__.py b/addons/resource/models/__init__.py index 43ace13cef0..9778b6b83ca 100644 --- a/addons/resource/models/__init__.py +++ b/addons/resource/models/__init__.py @@ -1,8 +1,11 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import resource -from . import resource_mixin from . import res_company from . import res_users +from . import resource_calendar +from . import resource_calendar_attendance +from . import resource_calendar_leaves +from . import resource_mixin +from . import resource_resource from . import utils diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource_calendar.py similarity index 61% rename from addons/resource/models/resource.py rename to addons/resource/models/resource_calendar.py index a29e00dd634..1449499e522 100644 --- a/addons/resource/models/resource.py +++ b/addons/resource/models/resource_calendar.py @@ -1,14 +1,15 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from collections import defaultdict import itertools -import math -from datetime import datetime, time, timedelta -from dateutil.relativedelta import relativedelta -from dateutil.rrule import rrule, DAILY, WEEKLY + +from collections import defaultdict +from datetime import datetime, timedelta from functools import partial from itertools import chain + +from dateutil.relativedelta import relativedelta +from dateutil.rrule import rrule, DAILY from pytz import timezone, utc from odoo import api, fields, models, _ @@ -18,133 +19,8 @@ from odoo.osv import expression from odoo.tools.float_utils import float_round from odoo.tools import date_utils, float_utils -from .resource_mixin import timezone_datetime +from .utils import Intervals, float_to_time, make_aware, datetime_to_string, string_to_datetime, ROUNDING_FACTOR -# 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): - """ Return ``dt`` with an explicit timezone, together with a function to - convert a datetime to the same (naive or aware) timezone as ``dt``. - """ - if dt.tzinfo: - return dt, lambda val: val.astimezone(dt.tzinfo) - else: - return dt.replace(tzinfo=utc), lambda val: val.astimezone(utc).replace(tzinfo=None) - - -def string_to_datetime(value): - """ Convert the given string value to a datetime in UTC. """ - return utc.localize(fields.Datetime.from_string(value)) - - -def datetime_to_string(dt): - """ Convert the given datetime (converted in UTC) to a string value. """ - return fields.Datetime.to_string(dt.astimezone(utc)) - - -def float_to_time(hours): - """ Convert a number of hours into a time object. """ - if hours == 24.0: - return time.max - fractional, integral = math.modf(hours) - return time(int(integral), int(float_round(60 * fractional, precision_digits=0)), 0) - - -def _boundaries(intervals, opening, closing): - """ Iterate on the boundaries of intervals. """ - for start, stop, recs in intervals: - if start < stop: - yield (start, opening, recs) - yield (stop, closing, recs) - -# YTI TODO LAST SPLIT FILES PLIIIIZ -class Intervals(object): - """ Collection of ordered disjoint intervals with some associated records. - Each interval is a triple ``(start, stop, records)``, where ``records`` - is a recordset. - """ - def __init__(self, intervals=()): - self._items = [] - if intervals: - # normalize the representation of intervals - append = self._items.append - starts = [] - recses = [] - for value, flag, recs in sorted(_boundaries(intervals, 'start', 'stop')): - if flag == 'start': - starts.append(value) - recses.append(recs) - else: - start = starts.pop() - if not starts: - append((start, value, recses[0].union(*recses))) - recses.clear() - - def __bool__(self): - return bool(self._items) - - def __len__(self): - return len(self._items) - - def __iter__(self): - return iter(self._items) - - def __reversed__(self): - return reversed(self._items) - - def __or__(self, other): - """ Return the union of two sets of intervals. """ - return Intervals(chain(self._items, other._items)) - - def __and__(self, other): - """ Return the intersection of two sets of intervals. """ - return self._merge(other, False) - - def __sub__(self, other): - """ Return the difference of two sets of intervals. """ - return self._merge(other, True) - - def _merge(self, other, difference): - """ Return the difference or intersection of two sets of intervals. """ - result = Intervals() - append = result._items.append - - # using 'self' and 'other' below forces normalization - bounds1 = _boundaries(self, 'start', 'stop') - bounds2 = _boundaries(other, 'switch', 'switch') - - start = None # set by start/stop - recs1 = None # set by start - enabled = difference # changed by switch - for value, flag, recs in sorted(chain(bounds1, bounds2)): - if flag == 'start': - start = value - recs1 = recs - elif flag == 'stop': - if enabled and start < value: - append((start, value, recs1)) - start = None - else: - if not enabled and start is not None: - start = value - if enabled and start is not None and start < value: - append((start, value, recs1)) - enabled = not enabled - - return result - -def sum_intervals(intervals): - """ Sum the intervals duration (unit : hour)""" - return sum( - (stop - start).total_seconds() / 3600 - for start, stop, meta in intervals - ) class ResourceCalendar(models.Model): """ Calendar model for a resource. It has @@ -162,7 +38,7 @@ class ResourceCalendar(models.Model): @api.model def default_get(self, fields): - res = super(ResourceCalendar, self).default_get(fields) + res = super().default_get(fields) if not res.get('name') and res.get('company_id'): res['name'] = _('Working Hours of %s', self.env['res.company'].browse(res['company_id']).name) if 'attendance_ids' in fields and not res.get('attendance_ids'): @@ -263,7 +139,7 @@ class ResourceCalendar(models.Model): default = {} if not default.get('name'): default.update(name=_('%s (copy)') % (self.name)) - return super(ResourceCalendar, self).copy(default) + return super().copy(default) @api.constrains('attendance_ids') def _check_attendance_ids(self): @@ -838,331 +714,3 @@ class ResourceCalendar(models.Model): for attendance in self.attendance_ids.filtered(lambda a: a.day_period != 'lunch' and ((not a.date_from or not a.date_to) or (a.date_from <= end.date() and a.date_to >= start.date()))): mapped_data[(attendance.week_type, attendance.dayofweek)] += attendance.hour_to - attendance.hour_from return max(mapped_data.values()) - - -class ResourceCalendarAttendance(models.Model): - _name = "resource.calendar.attendance" - _description = "Work Detail" - _order = 'week_type, dayofweek, hour_from' - - name = fields.Char(required=True) - dayofweek = fields.Selection([ - ('0', 'Monday'), - ('1', 'Tuesday'), - ('2', 'Wednesday'), - ('3', 'Thursday'), - ('4', 'Friday'), - ('5', 'Saturday'), - ('6', 'Sunday') - ], 'Day of Week', required=True, index=True, default='0') - date_from = fields.Date(string='Starting Date') - date_to = fields.Date(string='End Date') - hour_from = fields.Float(string='Work from', required=True, index=True, - help="Start and End time of working.\n" - "A specific value of 24:00 is interpreted as 23:59:59.999999.") - 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'), - ('lunch', 'Lunch'), - ('afternoon', 'Afternoon')], required=True, default='morning') - resource_id = fields.Many2one('resource.resource', 'Resource') - week_type = fields.Selection([ - ('1', 'Second'), - ('0', 'First') - ], 'Week Number', default=False) - two_weeks_calendar = fields.Boolean("Calendar in 2 weeks mode", related='calendar_id.two_weeks_calendar') - display_type = fields.Selection([ - ('line_section', "Section")], default=False, help="Technical field for UX purpose.") - sequence = fields.Integer(default=10, - help="Gives the sequence of this line when displaying the resource calendar.") - - @api.onchange('hour_from', 'hour_to') - def _onchange_hours(self): - # avoid negative or after midnight - self.hour_from = min(self.hour_from, 23.99) - self.hour_from = max(self.hour_from, 0.0) - self.hour_to = min(self.hour_to, 24) - self.hour_to = max(self.hour_to, 0.0) - - # avoid wrong order - self.hour_to = max(self.hour_to, self.hour_from) - - @api.model - def get_week_type(self, date): - # week_type is defined by - # * counting the number of days from January 1 of year 1 - # (extrapolated to dates prior to the first adoption of the Gregorian calendar) - # * converted to week numbers and then the parity of this number is asserted. - # It ensures that an even week number always follows an odd week number. With classical week number, - # some years have 53 weeks. Therefore, two consecutive odd week number follow each other (53 --> 1). - return int(math.floor((date.toordinal() - 1) / 7) % 2) - - def _compute_display_name(self): - super()._compute_display_name() - this_week_type = str(self.get_week_type(fields.Date.context_today(self))) - section_names = {'0': _('First week'), '1': _('Second week')} - section_info = {True: _('this week'), False: _('other week')} - for record in self.filtered(lambda l: l.display_type == 'line_section'): - section_name = "%s (%s)" % (section_names[record.week_type], section_info[this_week_type == record.week_type]) - record.display_name = section_name - - def _copy_attendance_vals(self): - self.ensure_one() - return { - 'name': self.name, - 'dayofweek': self.dayofweek, - 'date_from': self.date_from, - 'date_to': self.date_to, - 'hour_from': self.hour_from, - 'hour_to': self.hour_to, - 'day_period': self.day_period, - 'week_type': self.week_type, - 'display_type': self.display_type, - 'sequence': self.sequence, - } - - -class ResourceResource(models.Model): - _name = "resource.resource" - _description = "Resources" - _order = "name" - - @api.model - def default_get(self, fields): - res = super(ResourceResource, self).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, - required=True, 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(ResourceResource, self).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.update(name=_('%s (copy)') % (self.name)) - return super(ResourceResource, self).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_start = resource.calendar_id._get_closest_work_time(start, resource=resource, search_range=search_range, - compute_leaves=compute_leaves) - search_range[0] = start - calendar_end = resource.calendar_id._get_closest_work_time(end if end > start else start, 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] |= 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 - -class ResourceCalendarLeaves(models.Model): - _name = "resource.calendar.leaves" - _description = "Resource Time Off Detail" - _order = "date_from" - - def default_get(self, fields_list): - res = super().default_get(fields_list) - if 'date_from' in fields_list and 'date_to' in fields_list and not res.get('date_from') and not res.get('date_to'): - # Then we give the current day and we search the begin and end hours for this day in resource.calendar of the current company - today = fields.Datetime.now() - user_tz = timezone(self.env.user.tz or self._context.get('tz') or self.company_id.resource_calendar_id.tz or 'UTC') - date_from = user_tz.localize(datetime.combine(today, time.min)) - date_to = user_tz.localize(datetime.combine(today, time.max)) - intervals = self.env.company.resource_calendar_id._work_intervals_batch(date_from.replace(tzinfo=utc), date_to.replace(tzinfo=utc))[False] - if intervals: # Then we stop and return the dates given in parameter - list_intervals = [(start, stop) for start, stop, records in intervals] # Convert intervals in interval list - date_from = list_intervals[0][0] # We take the first date in the interval list - date_to = list_intervals[-1][1] # We take the last date in the interval list - res.update( - date_from=date_from.astimezone(utc).replace(tzinfo=None), - date_to=date_to.astimezone(utc).replace(tzinfo=None) - ) - return res - - name = fields.Char('Reason') - company_id = fields.Many2one( - 'res.company', string="Company", readonly=True, store=True, - default=lambda self: self.env.company, compute='_compute_company_id') - calendar_id = fields.Many2one('resource.calendar', 'Working Hours', domain="[('company_id', 'in', [company_id, False])]", check_company=True, index=True) - date_from = fields.Datetime('Start Date', required=True) - date_to = fields.Datetime('End Date', required=True) - resource_id = fields.Many2one( - "resource.resource", 'Resource', index=True, - help="If empty, this is a generic time off for the company. If a resource is set, the time off is only for this resource") - time_type = fields.Selection([('leave', 'Time Off'), ('other', 'Other')], default='leave', - help="Whether this should be computed as a time off or as work time (eg: formation)") - - @api.depends('calendar_id') - def _compute_company_id(self): - for leave in self: - leave.company_id = leave.calendar_id.company_id or self.env.company - - @api.constrains('date_from', 'date_to') - def check_dates(self): - if self.filtered(lambda leave: leave.date_from > leave.date_to): - raise ValidationError(_('The start date of the time off must be earlier than the end date.')) - - @api.onchange('resource_id') - def onchange_resource(self): - if self.resource_id: - self.calendar_id = self.resource_id.calendar_id - - def _copy_leave_vals(self): - self.ensure_one() - return { - 'name': self.name, - 'date_from': self.date_from, - 'date_to': self.date_to, - 'time_type': self.time_type, - } diff --git a/addons/resource/models/resource_calendar_attendance.py b/addons/resource/models/resource_calendar_attendance.py new file mode 100644 index 00000000000..859673f5f8d --- /dev/null +++ b/addons/resource/models/resource_calendar_attendance.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import math + +from odoo import api, fields, models, _ + + +class ResourceCalendarAttendance(models.Model): + _name = "resource.calendar.attendance" + _description = "Work Detail" + _order = 'week_type, dayofweek, hour_from' + + name = fields.Char(required=True) + dayofweek = fields.Selection([ + ('0', 'Monday'), + ('1', 'Tuesday'), + ('2', 'Wednesday'), + ('3', 'Thursday'), + ('4', 'Friday'), + ('5', 'Saturday'), + ('6', 'Sunday') + ], 'Day of Week', required=True, index=True, default='0') + date_from = fields.Date(string='Starting Date') + date_to = fields.Date(string='End Date') + hour_from = fields.Float(string='Work from', required=True, index=True, + help="Start and End time of working.\n" + "A specific value of 24:00 is interpreted as 23:59:59.999999.") + 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'), + ('lunch', 'Lunch'), + ('afternoon', 'Afternoon')], required=True, default='morning') + resource_id = fields.Many2one('resource.resource', 'Resource') + week_type = fields.Selection([ + ('1', 'Second'), + ('0', 'First') + ], 'Week Number', default=False) + two_weeks_calendar = fields.Boolean("Calendar in 2 weeks mode", related='calendar_id.two_weeks_calendar') + display_type = fields.Selection([ + ('line_section', "Section")], default=False, help="Technical field for UX purpose.") + sequence = fields.Integer(default=10, + help="Gives the sequence of this line when displaying the resource calendar.") + + @api.onchange('hour_from', 'hour_to') + def _onchange_hours(self): + # avoid negative or after midnight + self.hour_from = min(self.hour_from, 23.99) + self.hour_from = max(self.hour_from, 0.0) + self.hour_to = min(self.hour_to, 24) + self.hour_to = max(self.hour_to, 0.0) + + # avoid wrong order + self.hour_to = max(self.hour_to, self.hour_from) + + @api.model + def get_week_type(self, date): + # week_type is defined by + # * counting the number of days from January 1 of year 1 + # (extrapolated to dates prior to the first adoption of the Gregorian calendar) + # * converted to week numbers and then the parity of this number is asserted. + # It ensures that an even week number always follows an odd week number. With classical week number, + # some years have 53 weeks. Therefore, two consecutive odd week number follow each other (53 --> 1). + return int(math.floor((date.toordinal() - 1) / 7) % 2) + + def _compute_display_name(self): + super()._compute_display_name() + this_week_type = str(self.get_week_type(fields.Date.context_today(self))) + section_names = {'0': _('First week'), '1': _('Second week')} + section_info = {True: _('this week'), False: _('other week')} + for record in self.filtered(lambda l: l.display_type == 'line_section'): + section_name = "%s (%s)" % (section_names[record.week_type], section_info[this_week_type == record.week_type]) + record.display_name = section_name + + def _copy_attendance_vals(self): + self.ensure_one() + return { + 'name': self.name, + 'dayofweek': self.dayofweek, + 'date_from': self.date_from, + 'date_to': self.date_to, + 'hour_from': self.hour_from, + 'hour_to': self.hour_to, + 'day_period': self.day_period, + 'week_type': self.week_type, + 'display_type': self.display_type, + 'sequence': self.sequence, + } diff --git a/addons/resource/models/resource_calendar_leaves.py b/addons/resource/models/resource_calendar_leaves.py new file mode 100644 index 00000000000..4a20960e7f5 --- /dev/null +++ b/addons/resource/models/resource_calendar_leaves.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime, time +from pytz import timezone, utc + +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError + + +class ResourceCalendarLeaves(models.Model): + _name = "resource.calendar.leaves" + _description = "Resource Time Off Detail" + _order = "date_from" + + def default_get(self, fields_list): + res = super().default_get(fields_list) + if 'date_from' in fields_list and 'date_to' in fields_list and not res.get('date_from') and not res.get('date_to'): + # Then we give the current day and we search the begin and end hours for this day in resource.calendar of the current company + today = fields.Datetime.now() + user_tz = timezone(self.env.user.tz or self._context.get('tz') or self.company_id.resource_calendar_id.tz or 'UTC') + date_from = user_tz.localize(datetime.combine(today, time.min)) + date_to = user_tz.localize(datetime.combine(today, time.max)) + intervals = self.env.company.resource_calendar_id._work_intervals_batch(date_from.replace(tzinfo=utc), date_to.replace(tzinfo=utc))[False] + if intervals: # Then we stop and return the dates given in parameter + list_intervals = [(start, stop) for start, stop, records in intervals] # Convert intervals in interval list + date_from = list_intervals[0][0] # We take the first date in the interval list + date_to = list_intervals[-1][1] # We take the last date in the interval list + res.update( + date_from=date_from.astimezone(utc).replace(tzinfo=None), + date_to=date_to.astimezone(utc).replace(tzinfo=None) + ) + return res + + name = fields.Char('Reason') + company_id = fields.Many2one( + 'res.company', string="Company", readonly=True, store=True, + default=lambda self: self.env.company, compute='_compute_company_id') + calendar_id = fields.Many2one('resource.calendar', 'Working Hours', domain="[('company_id', 'in', [company_id, False])]", check_company=True, index=True) + date_from = fields.Datetime('Start Date', required=True) + date_to = fields.Datetime('End Date', required=True) + resource_id = fields.Many2one( + "resource.resource", 'Resource', index=True, + help="If empty, this is a generic time off for the company. If a resource is set, the time off is only for this resource") + time_type = fields.Selection([('leave', 'Time Off'), ('other', 'Other')], default='leave', + help="Whether this should be computed as a time off or as work time (eg: formation)") + + @api.depends('calendar_id') + def _compute_company_id(self): + for leave in self: + leave.company_id = leave.calendar_id.company_id or self.env.company + + @api.constrains('date_from', 'date_to') + def check_dates(self): + if self.filtered(lambda leave: leave.date_from > leave.date_to): + raise ValidationError(_('The start date of the time off must be earlier than the end date.')) + + @api.onchange('resource_id') + def onchange_resource(self): + if self.resource_id: + self.calendar_id = self.resource_id.calendar_id + + def _copy_leave_vals(self): + self.ensure_one() + return { + 'name': self.name, + 'date_from': self.date_from, + 'date_to': self.date_to, + 'time_type': self.time_type, + } diff --git a/addons/resource/models/resource_mixin.py b/addons/resource/models/resource_mixin.py index 934ce18260a..8b45c405442 100644 --- a/addons/resource/models/resource_mixin.py +++ b/addons/resource/models/resource_mixin.py @@ -2,16 +2,10 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from collections import defaultdict -from dateutil.relativedelta import relativedelta from pytz import utc from odoo import api, fields, models - - -def timezone_datetime(time): - if not time.tzinfo: - time = time.replace(tzinfo=utc) - return time +from .utils import timezone_datetime class ResourceMixin(models.AbstractModel): @@ -80,7 +74,7 @@ class ResourceMixin(models.AbstractModel): 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) + return super().copy_data(default) def _get_work_days_data_batch(self, from_datetime, to_datetime, compute_leaves=True, calendar=None, domain=None): """ diff --git a/addons/resource/models/resource_resource.py b/addons/resource/models/resource_resource.py new file mode 100644 index 00000000000..ed86c885564 --- /dev/null +++ b/addons/resource/models/resource_resource.py @@ -0,0 +1,195 @@ +# -*- 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, + required=True, 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.update(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_start = resource.calendar_id._get_closest_work_time(start, resource=resource, search_range=search_range, + compute_leaves=compute_leaves) + search_range[0] = start + calendar_end = resource.calendar_id._get_closest_work_time(end if end > start else start, 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] |= 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 diff --git a/addons/resource/models/utils.py b/addons/resource/models/utils.py index 8db355daeba..8cce56d4afa 100644 --- a/addons/resource/models/utils.py +++ b/addons/resource/models/utils.py @@ -1,6 +1,56 @@ +# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import math +from datetime import time +from itertools import chain +from pytz import utc + +from odoo import fields from odoo.osv.expression import normalize_domain, NOT_OPERATOR, DOMAIN_OPERATORS +from odoo.tools.float_utils import float_round + +# 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): + """ Return ``dt`` with an explicit timezone, together with a function to + convert a datetime to the same (naive or aware) timezone as ``dt``. + """ + if dt.tzinfo: + return dt, lambda val: val.astimezone(dt.tzinfo) + return dt.replace(tzinfo=utc), lambda val: val.astimezone(utc).replace(tzinfo=None) + + +def string_to_datetime(value): + """ Convert the given string value to a datetime in UTC. """ + return utc.localize(fields.Datetime.from_string(value)) + + +def datetime_to_string(dt): + """ Convert the given datetime (converted in UTC) to a string value. """ + return fields.Datetime.to_string(dt.astimezone(utc)) + + +def float_to_time(hours): + """ Convert a number of hours into a time object. """ + if hours == 24.0: + return time.max + fractional, integral = math.modf(hours) + return time(int(integral), int(float_round(60 * fractional, precision_digits=0)), 0) + + +def _boundaries(intervals, opening, closing): + """ Iterate on the boundaries of intervals. """ + for start, stop, recs in intervals: + if start < stop: + yield (start, opening, recs) + yield (stop, closing, recs) def filter_domain_leaf(domain, leaf_check): """ @@ -41,3 +91,90 @@ def filter_domain_leaf(domain, leaf_check): operator = domain[0] if len(domain) > 1 else False domain = domain[1:] if len(domain) > 1 else domain return _filter_domain_leaf_recursive(domain, leaf_check, operator=operator)[0] + +class Intervals(object): + """ Collection of ordered disjoint intervals with some associated records. + Each interval is a triple ``(start, stop, records)``, where ``records`` + is a recordset. + """ + def __init__(self, intervals=()): + self._items = [] + if intervals: + # normalize the representation of intervals + append = self._items.append + starts = [] + recses = [] + for value, flag, recs in sorted(_boundaries(intervals, 'start', 'stop')): + if flag == 'start': + starts.append(value) + recses.append(recs) + else: + start = starts.pop() + if not starts: + append((start, value, recses[0].union(*recses))) + recses.clear() + + def __bool__(self): + return bool(self._items) + + def __len__(self): + return len(self._items) + + def __iter__(self): + return iter(self._items) + + def __reversed__(self): + return reversed(self._items) + + def __or__(self, other): + """ Return the union of two sets of intervals. """ + return Intervals(chain(self._items, other._items)) + + def __and__(self, other): + """ Return the intersection of two sets of intervals. """ + return self._merge(other, False) + + def __sub__(self, other): + """ Return the difference of two sets of intervals. """ + return self._merge(other, True) + + def _merge(self, other, difference): + """ Return the difference or intersection of two sets of intervals. """ + result = Intervals() + append = result._items.append + + # using 'self' and 'other' below forces normalization + bounds1 = _boundaries(self, 'start', 'stop') + bounds2 = _boundaries(other, 'switch', 'switch') + + start = None # set by start/stop + recs1 = None # set by start + enabled = difference # changed by switch + for value, flag, recs in sorted(chain(bounds1, bounds2)): + if flag == 'start': + start = value + recs1 = recs + elif flag == 'stop': + if enabled and start < value: + append((start, value, recs1)) + start = None + else: + if not enabled and start is not None: + start = value + if enabled and start is not None and start < value: + append((start, value, recs1)) + enabled = not enabled + + return result + +def sum_intervals(intervals): + """ Sum the intervals duration (unit : hour)""" + return sum( + (stop - start).total_seconds() / 3600 + for start, stop, meta in intervals + ) + +def timezone_datetime(time): + if not time.tzinfo: + time = time.replace(tzinfo=utc) + return time diff --git a/addons/resource/views/menuitems.xml b/addons/resource/views/menuitems.xml new file mode 100644 index 00000000000..44953339ff8 --- /dev/null +++ b/addons/resource/views/menuitems.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/addons/resource/views/resource_calendar_attendance_views.xml b/addons/resource/views/resource_calendar_attendance_views.xml new file mode 100644 index 00000000000..d9c53eac6a3 --- /dev/null +++ b/addons/resource/views/resource_calendar_attendance_views.xml @@ -0,0 +1,45 @@ + + + + resource.calendar.attendance.tree + resource.calendar.attendance + + + + + + + + + + + + + + + + + + + resource.calendar.attendance.form + resource.calendar.attendance + +
+ + + + + + + + +
+
+
+
diff --git a/addons/resource/views/resource_calendar_leaves_views.xml b/addons/resource/views/resource_calendar_leaves_views.xml new file mode 100644 index 00000000000..4027b1ef843 --- /dev/null +++ b/addons/resource/views/resource_calendar_leaves_views.xml @@ -0,0 +1,110 @@ + + + + resource.calendar.leaves.search + resource.calendar.leaves + + + + + + + + + + + + + + + + + + resource.calendar.leaves.calendar + resource.calendar.leaves + + + + + + + + + + + resource.calendar.leaves.form + resource.calendar.leaves + +
+ + + + + + + + + + + + + + + + +
+
+ + + resource.calendar.leaves.tree + resource.calendar.leaves + 1 + + + + + + + + + + + + + + Resource Time Off + resource.calendar.leaves + tree,form,calendar + + + + + Resource Time Off + resource.calendar.leaves + tree,form,calendar + { + 'default_calendar_id': active_id, + 'search_default_calendar_id': active_id} + + + + + Closing Days + resource.calendar.leaves + calendar,tree,form + [('calendar_id','=',active_id), ('resource_id','=',False)] + {'default_calendar_id': active_id} + + form + + + + Resources Time Off + resource.calendar.leaves + calendar,tree,form + [('calendar_id','=',active_id), ('resource_id','!=',False)] + {'default_calendar_id': active_id} + + form + +
diff --git a/addons/resource/views/resource_calendar_views.xml b/addons/resource/views/resource_calendar_views.xml new file mode 100644 index 00000000000..2c918523153 --- /dev/null +++ b/addons/resource/views/resource_calendar_views.xml @@ -0,0 +1,84 @@ + + + + resource.calendar.search + resource.calendar + + + + + + + + + + + + resource.calendar.form + resource.calendar + +
+ +
+
+ + +
+
+

+ +

+ + + + + + + + + + + + + + + +
+ +
+
+ + + resource.calendar.tree + resource.calendar + + + + + + + + + + Working Times + resource.calendar + tree,form + + + +

+ Define working hours and time table that could be scheduled to your project members +

+
+
+
diff --git a/addons/resource/views/resource_resource_views.xml b/addons/resource/views/resource_resource_views.xml new file mode 100644 index 00000000000..b54ac282f28 --- /dev/null +++ b/addons/resource/views/resource_resource_views.xml @@ -0,0 +1,92 @@ + + + + + + resource.resource.search + resource.resource + + + + + + + + + + + + + + + + + + + + + + + resource.resource.form + resource.resource + +
+ + + + + + + + + + + + + + + + + + +
+
+
+ + + resource.resource.tree + resource.resource + + + + + + + + + + + + + + + + Resources + resource.resource + tree,form + {} + + Resources allow you to create and manage resources that should be involved in a specific project phase. You can also set their efficiency level and workload based on their weekly working hours. + + + + Resources + resource.resource + tree,form + { + 'default_calendar_id': active_id, + 'search_default_calendar_id': active_id} + + Resources allow you to create and manage resources that should be involved in a specific project phase. You can also set their efficiency level and workload based on their weekly working hours. + +
diff --git a/addons/test_resource/tests/test_resource.py b/addons/test_resource/tests/test_resource.py index e47a7cf8e7e..03cb1e1f311 100644 --- a/addons/test_resource/tests/test_resource.py +++ b/addons/test_resource/tests/test_resource.py @@ -7,7 +7,7 @@ from pytz import timezone, utc from odoo import fields from odoo.exceptions import ValidationError -from odoo.addons.resource.models.resource import Intervals, sum_intervals +from odoo.addons.resource.models.utils import Intervals, sum_intervals from odoo.addons.test_resource.tests.common import TestResourceCommon from odoo.tests.common import TransactionCase diff --git a/addons/website_event_exhibitor/models/event_sponsor.py b/addons/website_event_exhibitor/models/event_sponsor.py index 0f70e8b3621..293854d1646 100644 --- a/addons/website_event_exhibitor/models/event_sponsor.py +++ b/addons/website_event_exhibitor/models/event_sponsor.py @@ -6,7 +6,7 @@ from pytz import timezone, utc from odoo import api, fields, models, _ from odoo.addons.http_routing.models.ir_http import slug -from odoo.addons.resource.models.resource import float_to_time +from odoo.addons.resource.models.utils import float_to_time from odoo.modules.module import get_resource_path from odoo.tools import is_html_empty from odoo.tools.translate import html_translate