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()