# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import _, api, fields, models from odoo.exceptions import ValidationError from calendar import monthrange from dateutil.relativedelta import relativedelta from dateutil.rrule import rrule, rruleset, DAILY, WEEKLY, MONTHLY, YEARLY, MO, TU, WE, TH, FR, SA, SU MONTHS = { 'january': 31, 'february': 28, 'march': 31, 'april': 30, 'may': 31, 'june': 30, 'july': 31, 'august': 31, 'september': 30, 'october': 31, 'november': 30, 'december': 31, } DAYS = { 'mon': MO, 'tue': TU, 'wed': WE, 'thu': TH, 'fri': FR, 'sat': SA, 'sun': SU, } WEEKS = { 'first': 1, 'second': 2, 'third': 3, 'last': 4, } class ProjectTaskRecurrence(models.Model): _name = 'project.task.recurrence' _description = 'Task Recurrence' task_ids = fields.One2many('project.task', 'recurrence_id', copy=False) next_recurrence_date = fields.Date() recurrence_left = fields.Integer(string="Number of Tasks Left to Create", copy=False) repeat_interval = fields.Integer(string='Repeat Every', default=1) repeat_unit = fields.Selection([ ('day', 'Days'), ('week', 'Weeks'), ('month', 'Months'), ('year', 'Years'), ], default='week') repeat_type = fields.Selection([ ('forever', 'Forever'), ('until', 'End Date'), ('after', 'Number of Repetitions'), ], default="forever", string="Until") repeat_until = fields.Date(string="End Date") repeat_number = fields.Integer(string="Repetitions") repeat_on_month = fields.Selection([ ('date', 'Date of the Month'), ('day', 'Day of the Month'), ]) repeat_on_year = fields.Selection([ ('date', 'Date of the Year'), ('day', 'Day of the Year'), ]) mon = fields.Boolean(string="Mon") tue = fields.Boolean(string="Tue") wed = fields.Boolean(string="Wed") thu = fields.Boolean(string="Thu") fri = fields.Boolean(string="Fri") sat = fields.Boolean(string="Sat") sun = fields.Boolean(string="Sun") repeat_day = fields.Selection([ (str(i), str(i)) for i in range(1, 32) ]) repeat_week = fields.Selection([ ('first', 'First'), ('second', 'Second'), ('third', 'Third'), ('last', 'Last'), ]) repeat_weekday = fields.Selection([ ('mon', 'Monday'), ('tue', 'Tuesday'), ('wed', 'Wednesday'), ('thu', 'Thursday'), ('fri', 'Friday'), ('sat', 'Saturday'), ('sun', 'Sunday'), ], string='Day Of The Week', readonly=False) repeat_month = fields.Selection([ ('january', 'January'), ('february', 'February'), ('march', 'March'), ('april', 'April'), ('may', 'May'), ('june', 'June'), ('july', 'July'), ('august', 'August'), ('september', 'September'), ('october', 'October'), ('november', 'November'), ('december', 'December'), ]) @api.constrains('repeat_unit', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun') def _check_recurrence_days(self): for project in self.filtered(lambda p: p.repeat_unit == 'week'): if not any([project.mon, project.tue, project.wed, project.thu, project.fri, project.sat, project.sun]): raise ValidationError(_('You should select a least one day')) @api.constrains('repeat_interval') def _check_repeat_interval(self): if self.filtered(lambda t: t.repeat_interval <= 0): raise ValidationError(_('The interval should be greater than 0')) @api.constrains('repeat_number', 'repeat_type') def _check_repeat_number(self): if self.filtered(lambda t: t.repeat_type == 'after' and t.repeat_number <= 0): raise ValidationError(_('Should repeat at least once')) @api.constrains('repeat_type', 'repeat_until') def _check_repeat_until_date(self): today = fields.Date.today() if self.filtered(lambda t: t.repeat_type == 'until' and t.repeat_until < today): raise ValidationError(_('The end date should be in the future')) @api.constrains('repeat_unit', 'repeat_on_month', 'repeat_day', 'repeat_type', 'repeat_until') def _check_repeat_until_month(self): if self.filtered(lambda r: r.repeat_type == 'until' and r.repeat_unit == 'month' and r.repeat_until and r.repeat_on_month == 'date' and int(r.repeat_day) > r.repeat_until.day and monthrange(r.repeat_until.year, r.repeat_until.month)[1] != r.repeat_until.day): raise ValidationError(_('The end date should be after the day of the month or the last day of the month')) @api.model def _get_recurring_fields(self): return ['message_partner_ids', 'company_id', 'description', 'displayed_image_id', 'email_cc', 'parent_id', 'partner_email', 'partner_id', 'partner_phone', 'planned_hours', 'project_id', 'display_project_id', 'project_privacy_visibility', 'sequence', 'tag_ids', 'recurrence_id', 'name', 'recurring_task', 'analytic_account_id', 'user_ids'] def _get_weekdays(self, n=1): self.ensure_one() if self.repeat_unit == 'week': return [fn(n) for day, fn in DAYS.items() if self[day]] return [DAYS.get(self.repeat_weekday)(n)] @api.model def _get_next_recurring_dates(self, date_start, repeat_interval, repeat_unit, repeat_type, repeat_until, repeat_on_month, repeat_on_year, weekdays, repeat_day, repeat_week, repeat_month, **kwargs): count = kwargs.get('count', 1) rrule_kwargs = {'interval': repeat_interval or 1, 'dtstart': date_start} repeat_day = int(repeat_day) start = False dates = [] if repeat_type == 'until': rrule_kwargs['until'] = repeat_until if repeat_until else fields.Date.today() else: rrule_kwargs['count'] = count if repeat_unit == 'week'\ or (repeat_unit == 'month' and repeat_on_month == 'day')\ or (repeat_unit == 'year' and repeat_on_year == 'day'): rrule_kwargs['byweekday'] = weekdays if repeat_unit == 'day': rrule_kwargs['freq'] = DAILY elif repeat_unit == 'month': rrule_kwargs['freq'] = MONTHLY if repeat_on_month == 'date': start = date_start - relativedelta(days=1) start = start.replace(day=min(repeat_day, monthrange(start.year, start.month)[1])) if start < date_start: # Ensure the next recurrence is in the future start += relativedelta(months=repeat_interval) start = start.replace(day=min(repeat_day, monthrange(start.year, start.month)[1])) can_generate_date = (lambda: start <= repeat_until) if repeat_type == 'until' else (lambda: len(dates) < count) while can_generate_date(): dates.append(start) start += relativedelta(months=repeat_interval) start = start.replace(day=min(repeat_day, monthrange(start.year, start.month)[1])) return dates elif repeat_unit == 'year': rrule_kwargs['freq'] = YEARLY month = list(MONTHS.keys()).index(repeat_month) + 1 if repeat_month else date_start.month repeat_month = repeat_month or list(MONTHS.keys())[month - 1] rrule_kwargs['bymonth'] = month if repeat_on_year == 'date': rrule_kwargs['bymonthday'] = min(repeat_day, MONTHS.get(repeat_month)) rrule_kwargs['bymonth'] = month else: rrule_kwargs['freq'] = WEEKLY rules = rrule(**rrule_kwargs) return list(rules) if rules else [] def _new_task_values(self, task): self.ensure_one() fields_to_copy = self._get_recurring_fields() task_values = task.read(fields_to_copy).pop() create_values = { field: value[0] if isinstance(value, tuple) else value for field, value in task_values.items() } create_values['stage_id'] = task.project_id.type_ids[0].id if task.project_id.type_ids else task.stage_id.id return create_values def _create_subtasks(self, task, new_task, depth=3): if depth == 0 or not task.child_ids: return children = [] child_recurrence = [] # copy the subtasks of the original task for child in task.child_ids: if child.recurrence_id and child.recurrence_id.id in child_recurrence: # The subtask has been generated by another subtask in the childs # This subtasks is skipped as it will be meant to be a copy of the first # task of the recurrence we just created. continue child_values = self._new_task_values(child) child_values['parent_id'] = new_task.id if child.recurrence_id: # The subtask has a recurrence, the recurrence is thus copied rather than used # with raw reference in order to decouple the recurrence of the initial subtask # from the recurrence of the copied subtask which will live its own life and generate # subsequent tasks. child_recurrence += [child.recurrence_id.id] child_values['recurrence_id'] = child.recurrence_id.copy().id if child.child_ids and depth > 1: # If child has childs in the following layer and we will have to copy layer, we have to # first create the new_child record in order to have a new parent_id reference for the # "grandchildren" tasks new_child = self.env['project.task'].sudo().create(child_values) self._create_subtasks(child, new_child, depth=depth - 1) else: children.append(child_values) self.env['project.task'].sudo().create(children) def _create_next_task(self): for recurrence in self: task = max(recurrence.sudo().task_ids, key=lambda t: t.id) create_values = recurrence._new_task_values(task) new_task = self.env['project.task'].sudo().create(create_values) recurrence._create_subtasks(task, new_task, depth=3) def _set_next_recurrence_date(self): today = fields.Date.today() tomorrow = today + relativedelta(days=1) for recurrence in self.filtered( lambda r: r.repeat_type == 'after' and r.recurrence_left >= 0 or r.repeat_type == 'until' and r.repeat_until >= today or r.repeat_type == 'forever' ): if recurrence.repeat_type == 'after' and recurrence.recurrence_left == 0: recurrence.next_recurrence_date = False else: next_date = self._get_next_recurring_dates(tomorrow, recurrence.repeat_interval, recurrence.repeat_unit, recurrence.repeat_type, recurrence.repeat_until, recurrence.repeat_on_month, recurrence.repeat_on_year, recurrence._get_weekdays(WEEKS.get(recurrence.repeat_week)), recurrence.repeat_day, recurrence.repeat_week, recurrence.repeat_month, count=1) recurrence.next_recurrence_date = next_date[0] if next_date else False @api.model def _cron_create_recurring_tasks(self): if not self.env.user.has_group('project.group_project_recurring_tasks'): return today = fields.Date.today() recurring_today = self.search([('next_recurrence_date', '<=', today)]) recurring_today._create_next_task() for recurrence in recurring_today.filtered(lambda r: r.repeat_type == 'after'): recurrence.recurrence_left -= 1 recurring_today._set_next_recurrence_date() @api.model_create_multi def create(self, vals_list): for vals in vals_list: if vals.get('repeat_number'): vals['recurrence_left'] = vals.get('repeat_number') recurrences = super().create(vals_list) recurrences._set_next_recurrence_date() return recurrences def write(self, vals): if vals.get('repeat_number'): vals['recurrence_left'] = vals.get('repeat_number') res = super(ProjectTaskRecurrence, self).write(vals) if 'next_recurrence_date' not in vals: self._set_next_recurrence_date() return res