diff options
| author | Kevin Baptiste <kba@odoo.com> | 2020-06-23 13:26:42 +0000 |
|---|---|---|
| committer | Kevin Baptiste <kba@odoo.com> | 2020-08-12 08:39:32 +0000 |
| commit | 1b62bd9508c58c5a4f2e41f2176d737a2e404656 (patch) | |
| tree | 61ed108841e5a9506d5bb3b49641c2d66733bbb4 /addons/project/models/project.py | |
| parent | 0dd48767623dd9e6e53d943e8b87d4bfd18ca53c (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.py | 250 |
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 # --------------------------------------------------- |
