Multi is the default api for methods, it is not necessary to explicitly decorate methods with it, adds clutter and most people use it because they see that the rest of the code uses it. Done with `find . -type f -name '*.py' | xargs sed -i '/@api.multi/d'`
204 lines
8.1 KiB
Python
204 lines
8.1 KiB
Python
# -*- 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.company,
|
|
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.company.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)
|
|
|
|
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
|