Files
odoo_source/addons/project/models/project_task_recurrence.py
T
William Braeckman 7563b448da [IMP] project: add personal task stages and multiple assignments
Adds personal stages to tasks.
Users assigned to tasks will be able to have their own pipeline to
handle their tasks independently of the project's pipeline.

The assignee field on Tasks has also been changed to a Many2many to
support multiple assignees on one task.

The same table is used to store those information, essentially a triplet
(task, user, stage), to ensure that:
1) personal stages only apply to tasks to which you are assigned to.
2) synchronizing to make sure that you don't have a personal stage for a
   task on which you are not assigned anymore.

Alongside those changes, some minor changes have also been made:
- Modified the task's tree view.
- Renamed the Tasks menu to 'My Tasks'.
- The default view is now the kanban view for the 'My Tasks' action.
- project_id is not required anymore, tasks with no project are
  considered 'private', those tasks are only visible to those that are
  assigned to it.
- Added tracking of both user_ids and depend_on_ids in the chatter.

Closes odoo/odoo#74087

Task ID: 2398734
2021-09-01 07:17:40 +00:00

272 lines
10 KiB
Python

# -*- 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 ['message_partner_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_ids'] = 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