diff options
| author | jbm-odoo <jbm@odoo.com> | 2019-03-06 09:01:01 +0000 |
|---|---|---|
| committer | Yannick Tivisse <yti@odoo.com> | 2019-06-25 08:02:14 +0000 |
| commit | 1e8db023685108a137593188e653d6bf8f8298a9 (patch) | |
| tree | e1ac88bac5a2c096de8d03064fe11f11286ec3fc /addons/resource/models | |
| parent | faf67303b35e6b15dfe2fd4e51402db0458f7e43 (diff) | |
[IMP] resource: Manage calendars over 2 weeks
Purpose
=======
In Belgium, we need to support some partial time on 2 weeks.
For instance, a mid time, means you could work on monday, tuesday
and wednesday on week1 and monday and tuesday on week 2. Others payroll
softwares manage this.
Specification
=============
Add a checkbox on resource calendar to manage 2 weeks. --> Consider even/odd weeks.
Add sections on calendars (Like sales orders)
Modify methods in resources to apply this behavor generically.
Check that attendances aren't overlapped
Write an integration test for holidays - benefits - payslip
to check all this, + "credit temps" and wage modification.
Note
====
For the sake of simplicity, we only manage calendars over 2 weeks
to consider the odd/even weeks and keep an easy implementation.
For more complicated calendars (eg: over a months with specific targetted days),
it's better to create specific allocation requests and apply specific leaves
for those days, as it would be the case for parental leaves.
closes odoo/odoo#31674
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
Diffstat (limited to 'addons/resource/models')
| -rw-r--r-- | addons/resource/models/resource.py | 148 |
1 files changed, 139 insertions, 9 deletions
diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py index 78f1623942d..6b195804f10 100644 --- a/addons/resource/models/resource.py +++ b/addons/resource/models/resource.py @@ -3,7 +3,8 @@ import math from datetime import datetime, time, timedelta -from dateutil.rrule import rrule, DAILY +from dateutil.relativedelta import relativedelta +from dateutil.rrule import rrule, DAILY, WEEKLY from functools import partial from itertools import chain from pytz import timezone, utc @@ -13,6 +14,7 @@ from odoo.addons.base.models.res_partner import _tz_get from odoo.exceptions import ValidationError from odoo.osv import expression from odoo.tools.float_utils import float_round +from odoo.tools import date_utils # Default hour per day value. The one should # only be used when the one from the calendar @@ -185,19 +187,126 @@ class ResourceCalendar(models.Model): _tz_get, string='Timezone', required=True, default=lambda self: self._context.get('tz') or self.env.user.tz or 'UTC', help="This field is used in order to define in which timezone the resources will work.") + two_weeks_calendar = fields.Boolean(string="Calendar in 2 weeks mode") + two_weeks_explanation = fields.Char('Explanation', compute="_compute_two_weeks_explanation") + @api.depends('two_weeks_calendar') + def _compute_two_weeks_explanation(self): + today = fields.Date.today() + week_type = _("odd") if int(math.floor((today.toordinal() - 1) / 7) % 2) else _("even") + first_day = date_utils.start_of(today, 'week') + last_day = date_utils.end_of(today, 'week') + self.two_weeks_explanation = "This week (from %s to %s) is an %s week." % (first_day, last_day, week_type) def _get_global_attendances(self): - return self.attendance_ids.filtered(lambda attendance: not attendance.date_from and not attendance.date_to and not attendance.resource_id) + return self.attendance_ids.filtered(lambda attendance: + not attendance.date_from and not attendance.date_to + and not attendance.resource_id and not attendance.display_type) + + def _compute_hours_per_day(self, attendances): + if not attendances: + return 0 - @api.onchange('attendance_ids') - def _onchange_hours_per_day(self): - attendances = self._get_global_attendances() hour_count = 0.0 for attendance in attendances: hour_count += attendance.hour_to - attendance.hour_from - if attendances: - self.hours_per_day = float_round(hour_count / float(len(set(attendances.mapped('dayofweek')))), precision_digits=2) + + if self.two_weeks_calendar: + number_of_days = len(set(attendances.filtered(lambda cal: cal.week_type == '1').mapped('dayofweek'))) + number_of_days += len(set(attendances.filtered(lambda cal: cal.week_type == '0').mapped('dayofweek'))) + else: + number_of_days = len(set(attendances.mapped('dayofweek'))) + + return float_round(hour_count / float(number_of_days), precision_digits=2) + + @api.onchange('attendance_ids', 'two_weeks_calendar') + def _onchange_hours_per_day(self): + attendances = self._get_global_attendances() + self.hours_per_day = self._compute_hours_per_day(attendances) + + def switch_calendar_type(self): + if not self.two_weeks_calendar: + self.attendance_ids.unlink() + self.attendance_ids = [ + (0, 0, { + 'name': 'Even week', + 'dayofweek': '0', + 'sequence': '0', + 'hour_from': 0, + 'day_period': 'morning', + 'week_type': '0', + 'hour_to': 0, + 'display_type': + 'line_section'}), + (0, 0, { + 'name': 'Odd week', + 'dayofweek': '0', + 'sequence': '25', + 'hour_from': 0, + 'day_period': + 'morning', + 'week_type': '1', + 'hour_to': 0, + 'display_type': 'line_section'}), + ] + + self.two_weeks_calendar = True + default_attendance = self.default_get('attendance_ids')['attendance_ids'] + for idx, att in enumerate(default_attendance): + att[2]["week_type"] = '0' + att[2]["sequence"] = idx + 1 + self.attendance_ids = default_attendance + for idx, att in enumerate(default_attendance): + att[2]["week_type"] = '1' + att[2]["sequence"] = idx + 26 + self.attendance_ids = default_attendance + else: + self.two_weeks_calendar = False + self.attendance_ids.unlink() + self.attendance_ids = self.default_get('attendance_ids')['attendance_ids'] + self._onchange_hours_per_day() + + @api.onchange('attendance_ids') + def _onchange_attendance_ids(self): + if not self.two_weeks_calendar: + return + + even_week_seq = self.attendance_ids.filtered(lambda att: att.display_type == 'line_section' and att.week_type == '0') + odd_week_seq = self.attendance_ids.filtered(lambda att: att.display_type == 'line_section' and att.week_type == '1') + if len(even_week_seq) != 1 or len(odd_week_seq) != 1: + raise ValidationError(_("You can't delete section between weeks.")) + + even_week_seq = even_week_seq.sequence + odd_week_seq = odd_week_seq.sequence + + for line in self.attendance_ids.filtered(lambda att: att.display_type is False): + if even_week_seq > odd_week_seq: + line.week_type = '1' if even_week_seq > line.sequence else '0' + else: + line.week_type = '0' if odd_week_seq > line.sequence else '1' + + def _check_overlap(self, attendance_ids): + """ attendance_ids correspond to attendance of a week, + will check for each day of week that there are no superimpose. """ + result = [] + for attendance in attendance_ids.filtered(lambda att: not att.date_from and not att.date_to): + # 0.000001 is added to each start hour to avoid to detect two contiguous intervals as superimposing. + # Indeed Intervals function will join 2 intervals with the start and stop hour corresponding. + result.append((int(attendance.dayofweek) * 24 + attendance.hour_from + 0.000001, int(attendance.dayofweek) * 24 + attendance.hour_to, attendance)) + + if len(Intervals(result)) != len(result): + raise ValidationError(_("Attendances can't overlap.")) + + @api.constrains('attendance_ids') + def _check_attendance(self): + # Avoid superimpose in attendance + for calendar in self: + attendance_ids = calendar.attendance_ids.filtered(lambda attendance: not attendance.resource_id and attendance.display_type is False) + if calendar.two_weeks_calendar: + calendar._check_overlap(attendance_ids.filtered(lambda attendance: attendance.week_type == '0')) + calendar._check_overlap(attendance_ids.filtered(lambda attendance: attendance.week_type == '1')) + else: + calendar._check_overlap(attendance_ids) # -------------------------------------------------- # Computation API @@ -214,6 +323,7 @@ class ResourceCalendar(models.Model): domain = expression.AND([domain, [ ('calendar_id', '=', self.id), ('resource_id', 'in', resource_ids), + ('display_type', '=', False), ]]) # express all dates and times in the resource's timezone @@ -230,9 +340,20 @@ class ResourceCalendar(models.Model): until = end_dt.date() if attendance.date_to: until = min(until, attendance.date_to) + if attendance.week_type: + start_week_type = int(math.floor((start.toordinal()-1)/7) % 2) + if start_week_type != int(attendance.week_type): + # start must be the week of the attendance + # if it's not the case, we must remove one week + start = start + relativedelta(weeks=-1) weekday = int(attendance.dayofweek) - for day in rrule(DAILY, start, until=until, byweekday=weekday): + if self.two_weeks_calendar and attendance.week_type: + days = rrule(WEEKLY, start, interval=2, until=until, byweekday=weekday) + else: + days = rrule(DAILY, start, until=until, byweekday=weekday) + + for day in days: # attendance hours are interpreted in the resource's timezone dt0 = tz.localize(combine(day, float_to_time(attendance.hour_from))) dt1 = tz.localize(combine(day, float_to_time(attendance.hour_to))) @@ -396,7 +517,7 @@ class ResourceCalendar(models.Model): class ResourceCalendarAttendance(models.Model): _name = "resource.calendar.attendance" _description = "Work Detail" - _order = 'dayofweek, hour_from' + _order = 'week_type, dayofweek, hour_from' name = fields.Char(required=True) dayofweek = fields.Selection([ @@ -417,6 +538,15 @@ class ResourceCalendarAttendance(models.Model): calendar_id = fields.Many2one("resource.calendar", string="Resource's Calendar", required=True, ondelete='cascade') day_period = fields.Selection([('morning', 'Morning'), ('afternoon', 'Afternoon')], required=True, default='morning') resource_id = fields.Many2one('resource.resource', 'Resource') + week_type = fields.Selection([ + ('1', 'Odd week'), + ('0', 'Even week') + ], 'Week Even/Odd', 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): |
