From 9944209ef75e3d162c4d688c52cb908fdf314c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 20 Jan 2017 18:01:59 +0100 Subject: [PATCH] [IMP] resource: add resource mixin adding fields and an API for resource Purpose is to replace existing inherits of resource.resource by an inherit on the mixin itself. The mixin offers several methods to compute working intervals, attendance, hours, ... This mixin will be improved in future commits when new methods that will be required with future improvements of various service apps. This commit also adds tests for the mixin itself. A new model is defined only for testing purpose to test the mixin. --- addons/resource/__init__.py | 1 + addons/resource/models/__init__.py | 2 + addons/resource/models/res_users.py | 14 +++ addons/resource/models/resource_mixin.py | 91 ++++++++++++++++++++ addons/resource/security/ir.model.access.csv | 1 + addons/resource/tests/test_resource.py | 76 ++++++++++++++++ addons/resource/tests/test_resource_model.py | 12 +++ 7 files changed, 197 insertions(+) create mode 100644 addons/resource/models/res_users.py create mode 100644 addons/resource/models/resource_mixin.py create mode 100644 addons/resource/tests/test_resource_model.py diff --git a/addons/resource/__init__.py b/addons/resource/__init__.py index 81fc81d29c9..ed17ad09e62 100644 --- a/addons/resource/__init__.py +++ b/addons/resource/__init__.py @@ -2,3 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import models +import tests.test_resource_model diff --git a/addons/resource/models/__init__.py b/addons/resource/models/__init__.py index 9cf3e07f76d..9d226347a00 100644 --- a/addons/resource/models/__init__.py +++ b/addons/resource/models/__init__.py @@ -2,4 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import resource +import resource_mixin import res_company +import res_users \ No newline at end of file diff --git a/addons/resource/models/res_users.py b/addons/resource/models/res_users.py new file mode 100644 index 00000000000..9793a560da0 --- /dev/null +++ b/addons/resource/models/res_users.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = 'res.users' + + resource_ids = fields.One2many( + 'resource.resource', 'user_id', 'Resources') + resource_calendar_id = fields.Many2one( + 'resource.calendar', 'Default Working Hours', + related='resource_ids.calendar_id') diff --git a/addons/resource/models/resource_mixin.py b/addons/resource/models/resource_mixin.py new file mode 100644 index 00000000000..bd9fcc2e640 --- /dev/null +++ b/addons/resource/models/resource_mixin.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import timedelta + +from odoo import api, fields, models + + +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) + 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') + + @api.model + def create(self, values): + if not values.get('resource_id'): + resource = self.env['resource.resource'].create({ + 'name': values.get(self._rec_name) + }) + values['resource_id'] = resource.id + return super(ResourceMixin, self).create(values) + + def get_work_days_count(self, from_datetime, to_datetime, calendar=None): + """ Return the number of work days for the resource, taking into account + leaves. An optional calendar can be given in case multiple calendars can + be used on the resource. """ + return self.get_work_days_data(from_datetime, to_datetime, calendar=calendar)['days'] + + def get_work_days_data(self, from_datetime, to_datetime, calendar=None): + days_count = 0.0 + total_work_time = timedelta() + calendar = calendar or self.resource_calendar_id + for day_intervals in calendar._iter_work_intervals( + from_datetime, to_datetime, self.resource_id.id, + compute_leaves=True): + theoric_hours = self.get_day_work_hours_count(day_intervals[0][0].date(), calendar=calendar) + work_time = sum((interval[1] - interval[0] for interval in day_intervals), timedelta()) + total_work_time += work_time + days_count += round((work_time.total_seconds() / 3600 / theoric_hours) * 4) / 4 + return { + 'days': days_count, + 'hours': total_work_time.total_seconds() / 3600, + } + + def iter_works(self, from_datetime, to_datetime, calendar=None): + calendar = calendar or self.resource_calendar_id + return calendar._iter_work_intervals(from_datetime, to_datetime, self.resource_id.id) + + def get_leaves_day_count(self, from_datetime, to_datetime, calendar=None): + """ Return the number of leave days for the resource, taking into account + attendances. An optional calendar can be given in case multiple calendars + can be used on the resource. """ + days_count = 0.0 + calendar = calendar or self.resource_calendar_id + for day_intervals in calendar._iter_leave_intervals(from_datetime, to_datetime, self.resource_id.id): + theoric_hours = self.get_day_work_hours_count(day_intervals[0][0].date(), calendar=calendar) + leave_time = sum((interval[1] - interval[0] for interval in day_intervals), timedelta()) + days_count += round((leave_time.total_seconds()/3600 / theoric_hours) * 4) / 4 + return days_count + + def iter_leaves(self, from_datetime, to_datetime, calendar=None): + calendar = calendar or self.resource_calendar_id + return calendar._iter_leave_intervals(from_datetime, to_datetime, self.resource_id.id) + + def get_start_work_hour(self, day_dt, calendar=None): + calendar = calendar or self.resource_calendar_id + work_intervals = calendar._get_day_work_intervals(day_dt, resource_id=self.resource_id.id) + return work_intervals and work_intervals[0][0] + + def get_end_work_hour(self, day_dt, calendar=None): + calendar = calendar or self.resource_calendar_id + work_intervals = calendar._get_day_work_intervals(day_dt, resource_id=self.resource_id.id) + return work_intervals and work_intervals[-1][1] + + def get_day_work_hours_count(self, day_date, calendar=None): + calendar = calendar or self.resource_calendar_id + attendances = calendar._get_day_attendances(day_date, False, False) + if not attendances: + return 0 + return sum(map(lambda i: float(i.hour_to) - float(i.hour_from), attendances)) diff --git a/addons/resource/security/ir.model.access.csv b/addons/resource/security/ir.model.access.csv index 34ca64a5e94..02fcaa44e82 100644 --- a/addons/resource/security/ir.model.access.csv +++ b/addons/resource/security/ir.model.access.csv @@ -7,3 +7,4 @@ access_resource_resource,resource.resource,model_resource_resource,base.group_sy access_resource_resource_all,resource.resource all,model_resource_resource,base.group_user,1,0,0,0 access_resource_calendar_leaves_user,resource.calendar.leaves,model_resource_calendar_leaves,base.group_user,1,1,1,1 access_resource_calendar_leaves,resource.calendar.leaves,model_resource_calendar_leaves,base.group_system,1,1,1,1 +access_resource_test_all,resource.test.all,model_resource_test,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/addons/resource/tests/test_resource.py b/addons/resource/tests/test_resource.py index 33c678492e1..3a545f5feef 100644 --- a/addons/resource/tests/test_resource.py +++ b/addons/resource/tests/test_resource.py @@ -458,3 +458,79 @@ class TestWorkDays(TestResourceCommon): self.assertFalse(r.calendar_id._is_work_day(date(1932, 11, 19), r.id)) # sat self.assertFalse(r.calendar_id._is_work_day(date(1932, 11, 20), r.id)) # sun self.assertFalse(r.calendar_id._is_work_day(date(1932, 12, 6), r.id)) # personal leave + + +class TestResourceMixin(TestResourceCommon): + + def setUp(self): + super(TestResourceMixin, self).setUp() + self.lost_user = self.env['res.users'].with_context( + no_reset_password=True, + mail_create_nosubscribe=True + ).create({ + 'name': 'Désiré Boideladodo', + 'login': 'desire', + 'tz': 'Indian/Reunion', + 'groups_id': [(6, 0, [self.env.ref('base.group_user').id])] + }) + self.test = self.env['resource.test'].with_context(default_resource_calendar_id=self.calendar.id).create({'name': 'Test'}) + + def test_basics(self): + self.assertEqual(self.env['resource.test'].create({'name': 'Test'}).resource_calendar_id, self.env.user.company_id.resource_calendar_id) + self.assertEqual(self.test.resource_calendar_id, self.calendar) + + def test_work_days_count(self): + # user in timezone UTC-9 asks for work hours + self.env.user.tz = 'US/Alaska' + + res = self.test.get_work_days_count( + to_naive_utc(Datetime.from_string('2013-02-12 06:00:00'), self.env.user), + to_naive_utc(Datetime.from_string('2013-02-22 23:00:00'), self.env.user)) + self.assertEqual(res, 3.75) # generic leaves, 3 hours + + res = self.test.get_work_days_count( + to_naive_utc(Datetime.from_string('2013-02-12 06:00:00'), self.env.user), + to_naive_utc(Datetime.from_string('2013-02-22 20:00:00'), self.env.user)) + self.assertEqual(res, 3.5) # last day is truncated of 3 hours on 12) + + self.env['resource.calendar.leaves'].create({ + 'name': 'Timezoned Leaves', + 'calendar_id': self.test.resource_calendar_id.id, + 'resource_id': self.test.resource_id.id, + 'date_from': to_naive_utc(Datetime.from_string('2013-02-13 10:00:00'), self.env.user), + 'date_to': to_naive_utc(Datetime.from_string('2013-02-17 12:00:00'), self.env.user) + }) + + res = self.test.get_work_days_count( + to_naive_utc(Datetime.from_string('2013-02-12 06:00:00'), self.env.user), + to_naive_utc(Datetime.from_string('2013-02-22 20:00:00'), self.env.user)) + self.assertEqual(res, 2.5) # one day is on leave and last day is truncated of 3 hours on 12) + + def test_work_days_count_timezones_ultra(self): + # user in timezone UTC+4 is attached to the resource and create leaves + self.test.resource_id.write({ + 'user_id': self.lost_user.id, + }) + reunion_leave = self.env['resource.calendar.leaves'].sudo(self.lost_user).create({ + 'name': 'Timezoned Leaves', + 'calendar_id': self.test.resource_calendar_id.id, + 'resource_id': self.test.resource_id.id, + 'date_from': to_naive_utc(Datetime.from_string('2013-02-12 10:00:00'), self.lost_user), + 'date_to': to_naive_utc(Datetime.from_string('2013-02-12 12:00:00'), self.lost_user) + }) + self.assertEqual(reunion_leave.tz, 'Indian/Reunion') + + # user in timezone UTC-9 read and manipulate leaves + self.env.user.tz = 'US/Alaska' + res = self.test.get_work_days_data( + to_naive_utc(Datetime.from_string('2013-02-12 06:00:00'), self.env.user), + to_naive_utc(Datetime.from_string('2013-02-12 20:00:00'), self.env.user)) + self.assertEqual(res['days'], 0.75) + self.assertEqual(res['hours'], 6.0) + + # user in timezone UTC+4 read and manipulate leaves + res = self.test.sudo(self.lost_user).get_work_days_data( + to_naive_utc(Datetime.from_string('2013-02-12 06:00:00'), self.env.user), + to_naive_utc(Datetime.from_string('2013-02-12 20:00:00'), self.env.user)) + self.assertEqual(res['days'], 0.75) + self.assertEqual(res['hours'], 6.0) diff --git a/addons/resource/tests/test_resource_model.py b/addons/resource/tests/test_resource_model.py new file mode 100644 index 00000000000..dad7e3cb10a --- /dev/null +++ b/addons/resource/tests/test_resource_model.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class MailTest(models.Model): + _description = 'Test Resource Model' + _name = 'resource.test' + _inherit = ['resource.mixin'] + + name = fields.Char()