summaryrefslogtreecommitdiff
path: root/addons/project/models/project_task_recurrence.py
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/project/models/project_task_recurrence.py
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/project/models/project_task_recurrence.py')
-rw-r--r--addons/project/models/project_task_recurrence.py271
1 files changed, 271 insertions, 0 deletions
diff --git a/addons/project/models/project_task_recurrence.py b/addons/project/models/project_task_recurrence.py
new file mode 100644
index 00000000..50d861a6
--- /dev/null
+++ b/addons/project/models/project_task_recurrence.py
@@ -0,0 +1,271 @@
+# -*- 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')
+ next_recurrence_date = fields.Date()
+ recurrence_left = fields.Integer(string="Number of tasks left to create")
+
+ 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):
+ raise ValidationError('The end date should be after the day of the month')
+
+ @api.model
+ def _get_recurring_fields(self):
+ return ['allowed_user_ids', 'company_id', 'description', 'displayed_image_id', 'email_cc',
+ 'parent_id', 'partner_email', 'partner_id', 'partner_phone', 'planned_hours',
+ 'project_id', 'project_privacy_visibility', 'sequence', 'tag_ids', 'recurrence_id',
+ 'name', 'recurring_task']
+
+ 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)
+ if repeat_type == 'until' and repeat_until > date_start:
+ delta = relativedelta(repeat_until, date_start)
+ count = delta.years * 12 + delta.months
+ for i in range(count):
+ start = start.replace(day=min(repeat_day, monthrange(start.year, start.month)[1]))
+ if i == 0 and start < date_start:
+ # Ensure the next recurrence is in the future
+ start += relativedelta(months=repeat_interval)
+ dates.append(start)
+ start += relativedelta(months=repeat_interval)
+ return dates
+ elif repeat_unit == 'year':
+ rrule_kwargs['freq'] = YEARLY
+ month = list(MONTHS.keys()).index(repeat_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
+ create_values['user_id'] = False
+ return create_values
+
+ def _create_next_task(self):
+ for recurrence in self:
+ task = recurrence.sudo().task_ids[-1]
+ create_values = recurrence._new_task_values(task)
+ new_task = self.env['project.task'].sudo().create(create_values)
+ if not new_task.parent_id and task.child_ids:
+ children = []
+ # copy the subtasks of the original task
+ for child in task.child_ids:
+ child_values = recurrence._new_task_values(child)
+ child_values['parent_id'] = new_task.id
+ children.append(child_values)
+ self.env['project.task'].create(children)
+
+ 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(), 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
+ def create(self, vals):
+ if vals.get('repeat_number'):
+ vals['recurrence_left'] = vals.get('repeat_number')
+ res = super(ProjectTaskRecurrence, self).create(vals)
+ res._set_next_recurrence_date()
+ return res
+
+ 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