summaryrefslogtreecommitdiff
path: root/addons/project/models/project.py
diff options
context:
space:
mode:
authorKevin Baptiste <kba@odoo.com>2020-06-23 13:26:42 +0000
committerKevin Baptiste <kba@odoo.com>2020-08-12 08:39:32 +0000
commit1b62bd9508c58c5a4f2e41f2176d737a2e404656 (patch)
tree61ed108841e5a9506d5bb3b49641c2d66733bbb4 /addons/project/models/project.py
parent0dd48767623dd9e6e53d943e8b87d4bfd18ca53c (diff)
[IMP] project: add recurring tasks
Some interventions are done on a regular basis (e.g. maintenance of fire alarms, safety inspections). Having tasks auto-generate would facilitate the process and would ensure that the next intervention isn't missed/forgotten. closes odoo/odoo#55517 Taskid: 2172156 Related: odoo/enterprise#12246 Related: odoo/upgrade#1604 Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
Diffstat (limited to 'addons/project/models/project.py')
-rw-r--r--addons/project/models/project.py250
1 files changed, 247 insertions, 3 deletions
diff --git a/addons/project/models/project.py b/addons/project/models/project.py
index 00ccd6dc760..325c446ae59 100644
--- a/addons/project/models/project.py
+++ b/addons/project/models/project.py
@@ -2,13 +2,15 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import ast
-from datetime import timedelta
+from datetime import timedelta, datetime
from random import randint
from odoo import api, fields, models, tools, SUPERUSER_ID, _
from odoo.exceptions import UserError, AccessError, ValidationError, RedirectWarning
-from odoo.tools.misc import format_date
+from odoo.tools.misc import format_date, get_lang
+from odoo.osv.expression import OR
+from .project_task_recurrence import DAYS, WEEKS
class ProjectTaskType(models.Model):
_name = 'project.task.type'
@@ -210,6 +212,7 @@ class Project(models.Model):
subtask_project_id = fields.Many2one('project.project', string='Sub-task Project', ondelete="restrict",
help="Project in which sub-tasks of the current project will be created. It can be the current project itself.")
allow_subtasks = fields.Boolean('Sub-tasks', default=lambda self: self.env.user.has_group('project.group_subtask_project'))
+ allow_recurring_tasks = fields.Boolean('Recurring Tasks', default=lambda self: self.env.user.has_group('project.group_project_recurring_tasks'))
# rating fields
rating_request_deadline = fields.Datetime(compute='_compute_rating_request_deadline', store=True)
@@ -339,6 +342,9 @@ class Project(models.Model):
for task in project.task_ids:
task.allowed_user_ids -= permission_removed
+ if 'allow_recurring_tasks' in vals and not vals.get('allow_recurring_tasks'):
+ self.env['project.task'].search([('project_id', 'in', self.ids), ('recurring_task', '=', True)]).write({'recurring_task': False})
+
if 'active' in vals:
# archiving/unarchiving a project does it on its tasks, too
self.with_context(active_test=False).mapped('tasks').write({'active': vals['active']})
@@ -596,6 +602,172 @@ class Task(models.Model):
# customer portal: include comment and incoming emails in communication history
website_message_ids = fields.One2many(domain=lambda self: [('model', '=', self._name), ('message_type', 'in', ['email', 'comment'])])
+ # recurrence fields
+ allow_recurring_tasks = fields.Boolean(related='project_id.allow_recurring_tasks')
+ recurring_task = fields.Boolean(string="Recurrent")
+ recurring_count = fields.Integer(string="Tasks in Recurrence", compute='_compute_recurring_count')
+ recurrence_id = fields.Many2one('project.task.recurrence', copy=False)
+ recurrence_update = fields.Selection([
+ ('this', 'This task'),
+ ('subsequent', 'This and following tasks'),
+ ('all', 'All tasks'),
+ ], default='this', store=False)
+ recurrence_message = fields.Char(string='Next Recurrencies', compute='_compute_recurrence_message')
+
+ repeat_interval = fields.Integer(string='Repeat Every', default=1, compute='_compute_repeat', readonly=False)
+ repeat_unit = fields.Selection([
+ ('day', 'Days'),
+ ('week', 'Weeks'),
+ ('month', 'Months'),
+ ('year', 'Years'),
+ ], default='week', compute='_compute_repeat', readonly=False)
+ repeat_type = fields.Selection([
+ ('forever', 'Forever'),
+ ('until', 'End Date'),
+ ('after', 'Number of Repetitions'),
+ ], default="forever", string="Until", compute='_compute_repeat', readonly=False)
+ repeat_until = fields.Date(string="End Date", compute='_compute_repeat', readonly=False)
+ repeat_number = fields.Integer(string="Repetitions", default=1, compute='_compute_repeat', readonly=False)
+
+ repeat_on_month = fields.Selection([
+ ('date', 'Date of the Month'),
+ ('day', 'Day of the Month'),
+ ], default='date', compute='_compute_repeat', readonly=False)
+
+ repeat_on_year = fields.Selection([
+ ('date', 'Date of the Year'),
+ ('day', 'Day of the Year'),
+ ], default='date', compute='_compute_repeat', readonly=False)
+
+ mon = fields.Boolean(string="Mon", compute='_compute_repeat', readonly=False)
+ tue = fields.Boolean(string="Tue", compute='_compute_repeat', readonly=False)
+ wed = fields.Boolean(string="Wed", compute='_compute_repeat', readonly=False)
+ thu = fields.Boolean(string="Thu", compute='_compute_repeat', readonly=False)
+ fri = fields.Boolean(string="Fri", compute='_compute_repeat', readonly=False)
+ sat = fields.Boolean(string="Sat", compute='_compute_repeat', readonly=False)
+ sun = fields.Boolean(string="Sun", compute='_compute_repeat', readonly=False)
+
+ repeat_day = fields.Selection([
+ (str(i), str(i)) for i in range(1, 32)
+ ], compute='_compute_repeat', readonly=False)
+ repeat_week = fields.Selection([
+ ('first', 'First'),
+ ('second', 'Second'),
+ ('third', 'Third'),
+ ('last', 'Last'),
+ ], default='first', compute='_compute_repeat', readonly=False)
+ repeat_weekday = fields.Selection([
+ ('mon', 'Monday'),
+ ('tue', 'Tuesday'),
+ ('wed', 'Wednesday'),
+ ('thu', 'Thursday'),
+ ('fri', 'Friday'),
+ ('sat', 'Saturday'),
+ ('sun', 'Sunday'),
+ ], string='Day Of The Week', compute='_compute_repeat', 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'),
+ ], compute='_compute_repeat', readonly=False)
+
+ repeat_show_dow = fields.Boolean(compute='_compute_repeat_visibility')
+ repeat_show_day = fields.Boolean(compute='_compute_repeat_visibility')
+ repeat_show_week = fields.Boolean(compute='_compute_repeat_visibility')
+ repeat_show_month = fields.Boolean(compute='_compute_repeat_visibility')
+
+ @api.model
+ def _get_recurrence_fields(self):
+ return ['repeat_interval', 'repeat_unit', 'repeat_type', 'repeat_until', 'repeat_number',
+ 'repeat_on_month', 'repeat_on_year', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat',
+ 'sun', 'repeat_day', 'repeat_week', 'repeat_month', 'repeat_weekday']
+
+ @api.depends('recurring_task', 'repeat_unit', 'repeat_on_month', 'repeat_on_year')
+ def _compute_repeat_visibility(self):
+ for task in self:
+ task.repeat_show_day = task.recurring_task and (task.repeat_unit == 'month' and task.repeat_on_month == 'date') or (task.repeat_unit == 'year' and task.repeat_on_year == 'date')
+ task.repeat_show_week = task.recurring_task and (task.repeat_unit == 'month' and task.repeat_on_month == 'day') or (task.repeat_unit == 'year' and task.repeat_on_year == 'day')
+ task.repeat_show_dow = task.recurring_task and task.repeat_unit == 'week'
+ task.repeat_show_month = task.recurring_task and task.repeat_unit == 'year'
+
+ @api.depends('recurring_task', 'repeat_unit')
+ def _compute_repeat(self):
+ rec_fields = self._get_recurrence_fields()
+ defaults = self.default_get(rec_fields)
+ for task in self:
+ for f in rec_fields:
+ if task.recurrence_id:
+ task[f] = task.recurrence_id[f]
+ else:
+ if task.recurring_task:
+ task[f] = defaults.get(f)
+ else:
+ task[f] = False
+
+ 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.depends(
+ 'recurring_task', 'repeat_interval', 'repeat_unit', 'repeat_type', 'repeat_until',
+ 'repeat_number', 'repeat_on_month', 'repeat_on_year', 'mon', 'tue', 'wed', 'thu', 'fri',
+ 'sat', 'sun', 'repeat_day', 'repeat_week', 'repeat_month', 'repeat_weekday')
+ def _compute_recurrence_message(self):
+ self.recurrence_message = False
+ for task in self.filtered(lambda t: t.recurring_task and t._is_recurrence_valid()):
+ date = fields.Date.today()
+ number_occurrences = min(5, task.repeat_number if task.repeat_type == 'after' else 5)
+ delta = task.repeat_interval if task.repeat_unit == 'day' else 1
+ recurring_dates = self.env['project.task.recurrence']._get_next_recurring_dates(
+ date + timedelta(days=delta),
+ task.repeat_interval,
+ task.repeat_unit,
+ task.repeat_type,
+ task.repeat_until,
+ task.repeat_on_month,
+ task.repeat_on_year,
+ task._get_weekdays(WEEKS.get(task.repeat_week)),
+ task.repeat_day,
+ task.repeat_week,
+ task.repeat_month,
+ count=number_occurrences)
+ date_format = self.env['res.lang']._lang_get(self.env.user.lang).date_format
+ task.recurrence_message = '<ul>'
+ for date in recurring_dates[:5]:
+ task.recurrence_message += '<li>%s</li>' % date.strftime(date_format)
+ if task.repeat_type == 'after' and task.repeat_number > 5 or task.repeat_type == 'forever' or len(recurring_dates) > 5:
+ task.recurrence_message += '<li>...</li>'
+ task.recurrence_message += '</ul>'
+ if task.repeat_type == 'until':
+ task.recurrence_message += _('<p><em>Number of tasks: %(tasks_count)s</em></p>') % {'tasks_count': len(recurring_dates)}
+
+ def _is_recurrence_valid(self):
+ self.ensure_one()
+ return self.repeat_interval > 0 and\
+ (not self.repeat_show_dow or self._get_weekdays()) and\
+ (self.repeat_type != 'after' or self.repeat_number) and\
+ (self.repeat_type != 'until' or self.repeat_until and self.repeat_until > fields.Date.today())
+
+ @api.depends('recurrence_id')
+ def _compute_recurring_count(self):
+ self.recurring_count = 0
+ recurring_tasks = self.filtered(lambda l: l.recurrence_id)
+ count = self.env['project.task'].read_group([('recurrence_id', 'in', recurring_tasks.recurrence_id.ids)], ['id'], 'recurrence_id')
+ tasks_count = {c.get('recurrence_id')[0]: c.get('recurrence_id_count') for c in count}
+ for task in recurring_tasks:
+ task.recurring_count = tasks_count.get(task.recurrence_id.id, 0)
+
@api.depends('partner_id.email')
def _compute_partner_email(self):
for task in self:
@@ -759,6 +931,8 @@ class Task(models.Model):
default = {}
if not default.get('name'):
default['name'] = _("%s (copy)", self.name)
+ if self.recurrence_id:
+ default['recurrence_id'] = self.recurrence_id.copy().id
return super(Task, self).copy(default)
@api.constrains('parent_id')
@@ -821,6 +995,25 @@ class Task(models.Model):
# ------------------------------------------------
# CRUD overrides
# ------------------------------------------------
+ @api.model
+ def default_get(self, default_fields):
+ vals = super(Task, self).default_get(default_fields)
+
+ days = list(DAYS.keys())
+ week_start = fields.Datetime.today().weekday()
+
+ if all([d in default_fields for d in days]):
+ vals[days[week_start]] = True
+ if 'repeat_day' in default_fields:
+ vals['repeat_day'] = str(fields.Datetime.today().day)
+ if 'repeat_month' in default_fields:
+ vals['repeat_month'] = self._fields.get('repeat_month').selection[fields.Datetime.today().month - 1][0]
+ if 'repeat_until' in default_fields:
+ vals['repeat_until'] = fields.Date.today() + timedelta(days=7)
+ if 'repeat_weekday' in default_fields:
+ vals['repeat_weekday'] = self._fields.get('repeat_weekday').selection[week_start][0]
+
+ return vals
@api.model_create_multi
def create(self, vals_list):
@@ -847,12 +1040,22 @@ class Task(models.Model):
if vals.get('stage_id'):
vals.update(self.update_date_end(vals['stage_id']))
vals['date_last_stage_update'] = fields.Datetime.now()
+ # recurrence
+ rec_fields = vals.keys() & self._get_recurrence_fields()
+ if rec_fields and vals.get('recurring_task') is True:
+ rec_values = {rec_field: vals[rec_field] for rec_field in rec_fields}
+ rec_values['next_recurrence_date'] = fields.Datetime.today()
+ recurrence = self.env['project.task.recurrence'].create(rec_values)
+ vals['recurrence_id'] = recurrence.id
return super().create(vals_list)
def write(self, vals):
now = fields.Datetime.now()
if 'parent_id' in vals and vals['parent_id'] in self.ids:
raise UserError(_("Sorry. You can't set a task as its parent task."))
+ if 'active' in vals and not vals.get('active') and any(self.mapped('recurrence_id')):
+ # TODO: show a dialog to stop the recurrence
+ raise UserError(_('You cannot archive recurring tasks. Please, disable the recurrence first.'))
# stage change: update date_last_stage_update
if 'stage_id' in vals:
vals.update(self.update_date_end(vals['stage_id']))
@@ -864,7 +1067,33 @@ class Task(models.Model):
if vals.get('user_id') and 'date_assign' not in vals:
vals['date_assign'] = now
- result = super(Task, self).write(vals)
+ # recurrence fields
+ rec_fields = vals.keys() & self._get_recurrence_fields()
+ if rec_fields:
+ rec_values = {rec_field: vals[rec_field] for rec_field in rec_fields}
+ for task in self:
+ if task.recurrence_id:
+ task.recurrence_id.write(rec_values)
+ elif vals.get('recurring_task'):
+ rec_values['next_recurrence_date'] = fields.Datetime.today()
+ recurrence = self.env['project.task.recurrence'].create(rec_values)
+ task.recurrence_id = recurrence.id
+
+ if 'recurring_task' in vals and not vals.get('recurring_task'):
+ self.recurrence_id.unlink()
+
+ tasks = self
+ recurrence_update = vals.pop('recurrence_update', 'this')
+ if recurrence_update != 'this':
+ recurrence_domain = []
+ if recurrence_update == 'subsequent':
+ for task in self:
+ recurrence_domain = OR([recurrence_domain, ['&', ('recurrence_id', '=', task.recurrence_id.id), ('create_date', '>=', task.create_date)]])
+ else:
+ recurrence_domain = [('recurrence_id', 'in', self.recurrence_id.ids)]
+ tasks |= self.env['project.task'].search(recurrence_domain)
+
+ result = super(Task, tasks).write(vals)
# rating on stage
if 'stage_id' in vals and vals.get('stage_id'):
self.filtered(lambda x: x.project_id.rating_status == 'stage')._send_task_rating_mail(force_send=True)
@@ -876,6 +1105,12 @@ class Task(models.Model):
return {'date_end': fields.Datetime.now()}
return {'date_end': False}
+ def unlink(self):
+ if any(self.mapped('recurrence_id')):
+ # TODO: show a dialog to stop the recurrence
+ raise UserError(_('You cannot delete recurring tasks. Please, disable the recurrence first.'))
+ return super().unlink()
+
# ---------------------------------------------------
# Subtasks
# ---------------------------------------------------
@@ -1106,6 +1341,15 @@ class Task(models.Model):
return action
+ def action_recurring_tasks(self):
+ return {
+ 'name': 'Tasks in Recurrence',
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'project.task',
+ 'view_mode': 'tree,form',
+ 'domain': [('recurrence_id', 'in', self.recurrence_id.ids)],
+ }
+
# ---------------------------------------------------
# Rating business
# ---------------------------------------------------