[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>
This commit is contained in:
@@ -40,6 +40,7 @@
|
||||
'wizard/project_task_type_delete_views.xml',
|
||||
],
|
||||
'demo': ['data/project_demo.xml'],
|
||||
'qweb': ['static/src/xml/project_templates.xml'],
|
||||
'test': [
|
||||
],
|
||||
'installable': True,
|
||||
|
||||
@@ -10,6 +10,16 @@
|
||||
<field name="numbercall">-1</field>
|
||||
</record>
|
||||
|
||||
<record id="ir_cron_recurring_tasks" model="ir.cron">
|
||||
<field name="name">Project: Create Recurring Tasks</field>
|
||||
<field name="model_id" ref="project.model_project_task_recurrence"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_create_recurring_tasks()</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="nextcall" eval="(DateTime.now().replace(hour=3, minute=0) + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')" />
|
||||
</record>
|
||||
|
||||
<!-- Task-related subtypes for messaging / Chatter -->
|
||||
<record id="mt_task_new" model="mail.message.subtype">
|
||||
<field name="name">Task Created</field>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import analytic_account
|
||||
from . import project_task_recurrence
|
||||
from . import project
|
||||
from . import res_config_settings
|
||||
from . import res_partner
|
||||
|
||||
@@ -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
|
||||
# ---------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
# -*- 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]))
|
||||
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
|
||||
create_values['user_id'] = False
|
||||
return create_values
|
||||
|
||||
def _create_next_task(self):
|
||||
for recurrence in self:
|
||||
task = self.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 = self._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'):
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
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
|
||||
@@ -11,3 +11,9 @@ class ResConfigSettings(models.TransientModel):
|
||||
module_hr_timesheet = fields.Boolean(string="Task Logs")
|
||||
group_subtask_project = fields.Boolean("Sub-tasks", implied_group="project.group_subtask_project")
|
||||
group_project_rating = fields.Boolean("Customer Ratings", implied_group='project.group_project_rating')
|
||||
group_project_recurring_tasks = fields.Boolean("Recurring Tasks", implied_group="project.group_project_recurring_tasks")
|
||||
|
||||
def set_values(self):
|
||||
if self.user_has_groups('project.group_project_recurring_tasks') != self.group_project_recurring_tasks:
|
||||
self.env['project.project'].sudo().search([]).write({'allow_recurring_tasks': self.group_project_recurring_tasks})
|
||||
super(ResConfigSettings, self).set_values()
|
||||
|
||||
@@ -24,3 +24,4 @@ access_account_analytic_account_manager,account.analytic.account,analytic.model_
|
||||
access_account_analytic_line_project,account.analytic.line project,analytic.model_account_analytic_line,project.group_project_manager,1,1,1,1
|
||||
access_project_delete_wizard,access_project_delete_wizard,model_project_delete_wizard,project.group_project_manager,1,1,1,1
|
||||
access_project_task_type_delete_wizard,project.task.type.delete.wizard,model_project_task_type_delete_wizard,project.group_project_manager,1,1,1,1
|
||||
access_project_task_recurrence,project.task.recurrence,model_project_task_recurrence,project.group_project_user,1,1,1,1
|
||||
|
||||
|
@@ -29,6 +29,11 @@
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
</record>
|
||||
|
||||
<record id="group_project_recurring_tasks" model="res.groups">
|
||||
<field name="name">Use Recurring Tasks</field>
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
</record>
|
||||
|
||||
<data noupdate="1">
|
||||
<record id="base.default_user" model="res.users">
|
||||
<field name="groups_id" eval="[(4,ref('project.group_project_manager'))]"/>
|
||||
|
||||
@@ -70,3 +70,7 @@
|
||||
margin-right: 150px;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.o_form_project_recurrence_message *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -37,3 +37,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.o_dow_widget {
|
||||
th {
|
||||
padding: 10px 10px 0 10px;
|
||||
}
|
||||
|
||||
.o_dow_days {
|
||||
td {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="day_of_week_widget">
|
||||
<table class="o_dow_widget">
|
||||
<thead>
|
||||
<tr>
|
||||
<th t-foreach="widget.labels" t-as="label">
|
||||
<t t-esc="label"/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="o_dow_days" />
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -1,4 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_project_base, test_project_flow, test_access_rights, test_project_ui
|
||||
from . import test_project_base, test_project_flow, test_access_rights, test_project_recurrence, test_project_ui
|
||||
from . import test_multicompany
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from odoo.tests.common import SavepointCase, Form
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo import fields
|
||||
|
||||
from datetime import date, datetime
|
||||
from dateutil.rrule import MO, TU, WE, TH, FR, SA, SU
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
class TestProjectrecurrence(SavepointCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestProjectrecurrence, cls).setUpClass()
|
||||
|
||||
cls.env.user.groups_id += cls.env.ref('project.group_project_recurring_tasks')
|
||||
|
||||
cls.stage_a = cls.env['project.task.type'].create({'name': 'a'})
|
||||
cls.stage_b = cls.env['project.task.type'].create({'name': 'b'})
|
||||
cls.project_recurring = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Recurring',
|
||||
'allow_recurring_tasks': True,
|
||||
'type_ids': [
|
||||
(4, cls.stage_a.id),
|
||||
(4, cls.stage_b.id),
|
||||
]
|
||||
})
|
||||
|
||||
def set_task_create_date(self, task_id, create_date):
|
||||
self.env.cr.execute("UPDATE project_task SET create_date=%s WHERE id=%s", (create_date, task_id))
|
||||
|
||||
def test_recurrence_simple(self):
|
||||
with patch.object(fields.Date, 'today', lambda: datetime(2020, 2, 1)):
|
||||
with Form(self.env['project.task']) as form:
|
||||
form.name = 'test recurring task'
|
||||
form.project_id = self.project_recurring
|
||||
|
||||
form.recurring_task = True
|
||||
form.repeat_interval = 5
|
||||
form.repeat_unit = 'month'
|
||||
form.repeat_type = 'after'
|
||||
form.repeat_number = 10
|
||||
form.repeat_on_month = 'date'
|
||||
form.repeat_day = '31'
|
||||
task = form.save()
|
||||
self.assertTrue(bool(task.recurrence_id), 'should create a recurrence')
|
||||
|
||||
task.write(dict(repeat_interval=2, repeat_number=11))
|
||||
self.assertEqual(task.recurrence_id.repeat_interval, 2, 'recurrence should be updated')
|
||||
self.assertEqual(task.recurrence_id.repeat_number, 11, 'recurrence should be updated')
|
||||
self.assertEqual(task.recurrence_id.recurrence_left, 11)
|
||||
self.assertEqual(task.recurrence_id.next_recurrence_date, date(2020, 2, 29))
|
||||
|
||||
task.recurring_task = False
|
||||
self.assertFalse(bool(task.recurrence_id), 'the recurrence should be deleted')
|
||||
|
||||
def test_recurrence_cron(self):
|
||||
with patch.object(fields.Date, 'today', lambda: datetime(2020, 1, 1)):
|
||||
form = Form(self.env['project.task'])
|
||||
form.name = 'test recurring task'
|
||||
form.description = 'my super recurring task bla bla bla'
|
||||
form.project_id = self.project_recurring
|
||||
form.date_deadline = datetime(2020, 2, 1)
|
||||
|
||||
form.recurring_task = True
|
||||
form.repeat_interval = 1
|
||||
form.repeat_unit = 'month'
|
||||
form.repeat_type = 'after'
|
||||
form.repeat_number = 2
|
||||
form.repeat_on_month = 'date'
|
||||
form.repeat_day = '15'
|
||||
task = form.save()
|
||||
task.planned_hours = 2
|
||||
|
||||
self.assertEqual(task.recurrence_id.next_recurrence_date, date(2020, 1, 15))
|
||||
self.assertEqual(self.env['project.task'].search_count([('project_id', '=', self.project_recurring.id)]), 1)
|
||||
self.env['project.task.recurrence']._cron_create_recurring_tasks()
|
||||
self.assertEqual(self.env['project.task'].search_count([('project_id', '=', self.project_recurring.id)]), 1, 'no extra task should be created')
|
||||
self.assertEqual(task.recurrence_id.recurrence_left, 2)
|
||||
|
||||
with patch.object(fields.Date, 'today', lambda: datetime(2020, 1, 15)):
|
||||
self.assertEqual(self.env['project.task'].search_count([('project_id', '=', self.project_recurring.id)]), 1)
|
||||
self.env['project.task.recurrence']._cron_create_recurring_tasks()
|
||||
self.assertEqual(self.env['project.task'].search_count([('project_id', '=', self.project_recurring.id)]), 2)
|
||||
self.assertEqual(task.recurrence_id.recurrence_left, 1)
|
||||
|
||||
with patch.object(fields.Date, 'today', lambda: datetime(2020, 2, 15)):
|
||||
self.env['project.task.recurrence']._cron_create_recurring_tasks()
|
||||
self.assertEqual(task.recurrence_id.recurrence_left, 0)
|
||||
|
||||
tasks = self.env['project.task'].search([('project_id', '=', self.project_recurring.id)])
|
||||
self.assertEqual(len(tasks), 3)
|
||||
|
||||
self.assertTrue(bool(tasks[2].date_deadline))
|
||||
self.assertFalse(tasks[1].date_deadline, "Deadline should not be copied")
|
||||
|
||||
for f in self.env['project.task.recurrence']._get_recurring_fields():
|
||||
self.assertTrue(tasks[0][f] == tasks[1][f] == tasks[2][f], "Field %s should have been copied" % f)
|
||||
|
||||
def test_recurrence_update_task(self):
|
||||
with patch.object(fields.Date, 'today', lambda: datetime(2020, 1, 1)):
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'test recurring task',
|
||||
'project_id': self.project_recurring.id,
|
||||
'recurring_task': True,
|
||||
'repeat_interval': 1,
|
||||
'repeat_unit': 'week',
|
||||
'repeat_type': 'after',
|
||||
'repeat_number': 2,
|
||||
'mon': True,
|
||||
})
|
||||
|
||||
with patch.object(fields.Date, 'today', lambda: datetime(2020, 1, 6)):
|
||||
self.env['project.task.recurrence']._cron_create_recurring_tasks()
|
||||
|
||||
with patch.object(fields.Date, 'today', lambda: datetime(2020, 1, 13)):
|
||||
self.env['project.task.recurrence']._cron_create_recurring_tasks()
|
||||
|
||||
task_c, task_b, task_a = self.env['project.task'].search([('project_id', '=', self.project_recurring.id)])
|
||||
|
||||
self.set_task_create_date(task_a.id, datetime(2020, 1, 1))
|
||||
self.set_task_create_date(task_b.id, datetime(2020, 1, 6))
|
||||
self.set_task_create_date(task_c.id, datetime(2020, 1, 13))
|
||||
(task_a+task_b+task_c).invalidate_cache()
|
||||
|
||||
task_c.write({
|
||||
'name': 'my super updated task',
|
||||
'recurrence_update': 'all',
|
||||
})
|
||||
|
||||
self.assertEqual(task_a.name, 'my super updated task')
|
||||
self.assertEqual(task_b.name, 'my super updated task')
|
||||
self.assertEqual(task_c.name, 'my super updated task')
|
||||
|
||||
task_a.write({
|
||||
'name': 'don\'t you dare change my title',
|
||||
'recurrence_update': 'this',
|
||||
})
|
||||
|
||||
self.assertEqual(task_a.name, 'don\'t you dare change my title')
|
||||
self.assertEqual(task_b.name, 'my super updated task')
|
||||
self.assertEqual(task_c.name, 'my super updated task')
|
||||
|
||||
task_b.write({
|
||||
'description': 'hello!',
|
||||
'recurrence_update': 'subsequent',
|
||||
})
|
||||
|
||||
self.assertEqual(task_a.description, False)
|
||||
self.assertEqual(task_b.description, '<p>hello!</p>')
|
||||
self.assertEqual(task_c.description, '<p>hello!</p>')
|
||||
|
||||
def test_recurrence_fields_visibility(self):
|
||||
form = Form(self.env['project.task'])
|
||||
|
||||
form.name = 'test recurring task'
|
||||
form.project_id = self.project_recurring
|
||||
form.recurring_task = True
|
||||
|
||||
form.repeat_unit = 'week'
|
||||
self.assertTrue(form.repeat_show_dow)
|
||||
self.assertFalse(form.repeat_show_day)
|
||||
self.assertFalse(form.repeat_show_week)
|
||||
self.assertFalse(form.repeat_show_month)
|
||||
|
||||
form.repeat_unit = 'month'
|
||||
form.repeat_on_month = 'date'
|
||||
self.assertFalse(form.repeat_show_dow)
|
||||
self.assertTrue(form.repeat_show_day)
|
||||
self.assertFalse(form.repeat_show_week)
|
||||
self.assertFalse(form.repeat_show_month)
|
||||
|
||||
form.repeat_unit = 'month'
|
||||
form.repeat_on_month = 'day'
|
||||
self.assertFalse(form.repeat_show_dow)
|
||||
self.assertFalse(form.repeat_show_day)
|
||||
self.assertTrue(form.repeat_show_week)
|
||||
self.assertFalse(form.repeat_show_month)
|
||||
|
||||
form.repeat_unit = 'year'
|
||||
form.repeat_on_year = 'date'
|
||||
self.assertFalse(form.repeat_show_dow)
|
||||
self.assertTrue(form.repeat_show_day)
|
||||
self.assertFalse(form.repeat_show_week)
|
||||
self.assertTrue(form.repeat_show_month)
|
||||
|
||||
form.repeat_unit = 'year'
|
||||
form.repeat_on_year = 'day'
|
||||
self.assertFalse(form.repeat_show_dow)
|
||||
self.assertFalse(form.repeat_show_day)
|
||||
self.assertTrue(form.repeat_show_week)
|
||||
self.assertTrue(form.repeat_show_month)
|
||||
|
||||
form.recurring_task = False
|
||||
self.assertFalse(form.repeat_show_dow)
|
||||
self.assertFalse(form.repeat_show_day)
|
||||
self.assertFalse(form.repeat_show_week)
|
||||
self.assertFalse(form.repeat_show_month)
|
||||
|
||||
def test_recurrence_week_day(self):
|
||||
form = Form(self.env['project.task'])
|
||||
|
||||
form.name = 'test recurring task'
|
||||
form.project_id = self.project_recurring
|
||||
form.recurring_task = True
|
||||
form.repeat_unit = 'week'
|
||||
|
||||
form.mon = False
|
||||
form.tue = False
|
||||
form.wed = False
|
||||
form.thu = False
|
||||
form.fri = False
|
||||
form.sat = False
|
||||
form.sun = False
|
||||
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
form.save()
|
||||
|
||||
def test_recurrence_next_dates_week(self):
|
||||
dates = self.env['project.task.recurrence']._get_next_recurring_dates(
|
||||
date_start=date(2020, 1, 1),
|
||||
repeat_interval=1,
|
||||
repeat_unit='week',
|
||||
repeat_type=False,
|
||||
repeat_until=False,
|
||||
repeat_on_month=False,
|
||||
repeat_on_year=False,
|
||||
weekdays=False,
|
||||
repeat_day=False,
|
||||
repeat_week=False,
|
||||
repeat_month=False,
|
||||
count=5)
|
||||
|
||||
self.assertEqual(dates[0], datetime(2020, 1, 6, 0, 0))
|
||||
self.assertEqual(dates[1], datetime(2020, 1, 13, 0, 0))
|
||||
self.assertEqual(dates[2], datetime(2020, 1, 20, 0, 0))
|
||||
self.assertEqual(dates[3], datetime(2020, 1, 27, 0, 0))
|
||||
self.assertEqual(dates[4], datetime(2020, 2, 3, 0, 0))
|
||||
|
||||
dates = self.env['project.task.recurrence']._get_next_recurring_dates(
|
||||
date_start=date(2020, 1, 1),
|
||||
repeat_interval=3,
|
||||
repeat_unit='week',
|
||||
repeat_type='until',
|
||||
repeat_until=date(2020, 2, 1),
|
||||
repeat_on_month=False,
|
||||
repeat_on_year=False,
|
||||
weekdays=[MO, FR],
|
||||
repeat_day=False,
|
||||
repeat_week=False,
|
||||
repeat_month=False,
|
||||
count=100)
|
||||
|
||||
self.assertEqual(len(dates), 3)
|
||||
self.assertEqual(dates[0], datetime(2020, 1, 3, 0, 0))
|
||||
self.assertEqual(dates[1], datetime(2020, 1, 20, 0, 0))
|
||||
self.assertEqual(dates[2], datetime(2020, 1, 24, 0, 0))
|
||||
|
||||
def test_recurrence_next_dates_month(self):
|
||||
dates = self.env['project.task.recurrence']._get_next_recurring_dates(
|
||||
date_start=date(2020, 1, 15),
|
||||
repeat_interval=1,
|
||||
repeat_unit='month',
|
||||
repeat_type=False, # Forever
|
||||
repeat_until=False,
|
||||
repeat_on_month='date',
|
||||
repeat_on_year=False,
|
||||
weekdays=False,
|
||||
repeat_day=31,
|
||||
repeat_week=False,
|
||||
repeat_month=False,
|
||||
count=12)
|
||||
|
||||
# should take the last day of each month
|
||||
self.assertEqual(dates[0], date(2020, 1, 31))
|
||||
self.assertEqual(dates[1], date(2020, 2, 29))
|
||||
self.assertEqual(dates[2], date(2020, 3, 31))
|
||||
self.assertEqual(dates[3], date(2020, 4, 30))
|
||||
self.assertEqual(dates[4], date(2020, 5, 31))
|
||||
self.assertEqual(dates[5], date(2020, 6, 30))
|
||||
self.assertEqual(dates[6], date(2020, 7, 31))
|
||||
self.assertEqual(dates[7], date(2020, 8, 31))
|
||||
self.assertEqual(dates[8], date(2020, 9, 30))
|
||||
self.assertEqual(dates[9], date(2020, 10, 31))
|
||||
self.assertEqual(dates[10], date(2020, 11, 30))
|
||||
self.assertEqual(dates[11], date(2020, 12, 31))
|
||||
|
||||
dates = self.env['project.task.recurrence']._get_next_recurring_dates(
|
||||
date_start=date(2020, 2, 20),
|
||||
repeat_interval=3,
|
||||
repeat_unit='month',
|
||||
repeat_type=False, # Forever
|
||||
repeat_until=False,
|
||||
repeat_on_month='date',
|
||||
repeat_on_year=False,
|
||||
weekdays=False,
|
||||
repeat_day=29,
|
||||
repeat_week=False,
|
||||
repeat_month=False,
|
||||
count=5)
|
||||
|
||||
self.assertEqual(dates[0], date(2020, 2, 29))
|
||||
self.assertEqual(dates[1], date(2020, 5, 29))
|
||||
self.assertEqual(dates[2], date(2020, 8, 29))
|
||||
self.assertEqual(dates[3], date(2020, 11, 29))
|
||||
self.assertEqual(dates[4], date(2021, 2, 28))
|
||||
|
||||
dates = self.env['project.task.recurrence']._get_next_recurring_dates(
|
||||
date_start=date(2020, 1, 10),
|
||||
repeat_interval=1,
|
||||
repeat_unit='month',
|
||||
repeat_type='until',
|
||||
repeat_until=datetime(2020, 5, 31),
|
||||
repeat_on_month='day',
|
||||
repeat_on_year=False,
|
||||
weekdays=[SA(4), ], # 4th Saturday
|
||||
repeat_day=29,
|
||||
repeat_week=False,
|
||||
repeat_month=False,
|
||||
count=6)
|
||||
|
||||
self.assertEqual(len(dates), 5)
|
||||
self.assertEqual(dates[0], datetime(2020, 1, 25))
|
||||
self.assertEqual(dates[1], datetime(2020, 2, 22))
|
||||
self.assertEqual(dates[2], datetime(2020, 3, 28))
|
||||
self.assertEqual(dates[3], datetime(2020, 4, 25))
|
||||
self.assertEqual(dates[4], datetime(2020, 5, 23))
|
||||
|
||||
def test_recurrence_next_dates_year(self):
|
||||
dates = self.env['project.task.recurrence']._get_next_recurring_dates(
|
||||
date_start=date(2020, 12, 1),
|
||||
repeat_interval=1,
|
||||
repeat_unit='year',
|
||||
repeat_type='until',
|
||||
repeat_until=datetime(2026, 1, 1),
|
||||
repeat_on_month=False,
|
||||
repeat_on_year='date',
|
||||
weekdays=False,
|
||||
repeat_day=31,
|
||||
repeat_week=False,
|
||||
repeat_month='november',
|
||||
count=10)
|
||||
|
||||
self.assertEqual(len(dates), 5)
|
||||
self.assertEqual(dates[0], datetime(2021, 11, 30))
|
||||
self.assertEqual(dates[1], datetime(2022, 11, 30))
|
||||
self.assertEqual(dates[2], datetime(2023, 11, 30))
|
||||
self.assertEqual(dates[3], datetime(2024, 11, 30))
|
||||
self.assertEqual(dates[4], datetime(2025, 11, 30))
|
||||
@@ -351,9 +351,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 o_setting_box" id="recurring_tasks_setting" groups="project.group_project_recurring_tasks">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="allow_recurring_tasks" />
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="allow_recurring_tasks" />
|
||||
<div class="text-muted">
|
||||
Auto-generate tasks for regular activities
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</page>
|
||||
</notebook>
|
||||
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" options="{'post_refresh':True}" help="Follow this project to automatically track the events associated to tasks and issues of this project." groups="base.group_user"/>
|
||||
</div>
|
||||
@@ -444,6 +456,7 @@
|
||||
<field name="name" class="o_project_name oe_inline"
|
||||
string="Project Name" placeholder="e.g. Office Party"/>
|
||||
<field name="user_id" invisible="1"/>
|
||||
<field name="allow_recurring_tasks" groups="project.group_project_recurring_tasks" />
|
||||
</group>
|
||||
<div name="alias_def" colspan="2" attrs="{'invisible': [('alias_domain', '=', False)]}">
|
||||
<label for="alias_enabled" class="oe_inline" string="Create tasks by sending an email to"/>
|
||||
@@ -607,6 +620,12 @@
|
||||
<form string="Task" class="o_form_project_tasks">
|
||||
<field name="allow_subtasks" invisible="1" />
|
||||
<field name="is_closed" invisible="1" />
|
||||
<field name="allow_recurring_tasks" invisible="1" />
|
||||
<field name="repeat_show_dow" invisible="1" />
|
||||
<field name="repeat_show_day" invisible="1" />
|
||||
<field name="repeat_show_week" invisible="1" />
|
||||
<field name="repeat_show_month" invisible="1" />
|
||||
<field name="recurrence_id" invisible="1" />
|
||||
<header>
|
||||
<button name="action_assign_to_me" string="Assign to Me" type="object" class="oe_highlight"
|
||||
attrs="{'invisible' : [('user_id', '!=', False)]}"/>
|
||||
@@ -615,6 +634,10 @@
|
||||
<div class="text-center alert alert-primary" role="alert" attrs="{'invisible': ['|', ('ribbon_message', '=', False), ('ribbon_message', '=', '')]}">
|
||||
<field name="ribbon_message"/>
|
||||
</div>
|
||||
<div class="alert alert-info oe_edit_only" role="status" attrs="{'invisible': ['|', ('recurring_task', '=', False), ('recurrence_id', '=', False)]}">
|
||||
<p>Edit recurring task</p>
|
||||
<field name="recurrence_update" widget="radio"/>
|
||||
</div>
|
||||
<sheet string="Task">
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_subtask" type="object" class="oe_stat_button" icon="fa-tasks"
|
||||
@@ -624,6 +647,15 @@
|
||||
<button name="%(rating_rating_action_task)d" type="action" attrs="{'invisible': [('rating_count', '=', 0)]}" class="oe_stat_button" icon="fa-smile-o" groups="project.group_project_rating">
|
||||
<field name="rating_count" string="Rating" widget="statinfo"/>
|
||||
</button>
|
||||
<button name="action_recurring_tasks" type="object" attrs="{'invisible': [('recurrence_id', '=', False)]}" class="oe_stat_button" icon="fa-repeat" groups="project.group_project_recurring_tasks">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="recurring_count" widget="statinfo" nolabel="1" />
|
||||
Tasks
|
||||
</span>
|
||||
<span class="o_stat_text">in Recurrence</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
|
||||
<div class="oe_title pr-0">
|
||||
@@ -654,6 +686,7 @@
|
||||
attrs="{'invisible' : [('allow_subtasks', '=', False)]}"
|
||||
/>
|
||||
<field name="date_deadline" widget="remaining_days" attrs="{'invisible': [('is_closed', '=', True)]}"/>
|
||||
<field name="recurring_task" attrs="{'invisible': [('allow_recurring_tasks', '=', False)]}" />
|
||||
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
|
||||
</group>
|
||||
</group>
|
||||
@@ -662,6 +695,51 @@
|
||||
<field name="description" type="html"/>
|
||||
<div class="oe_clear"/>
|
||||
</page>
|
||||
<page name="recurrence" string="Recurrence" attrs="{'invisible': [('recurring_task', '=', False)]}">
|
||||
<group>
|
||||
<group>
|
||||
<label for="repeat_interval" />
|
||||
<div class="o_row">
|
||||
<field name="repeat_interval" attrs="{'required': [('recurring_task', '=', True)]}" />
|
||||
<field name="repeat_unit" attrs="{'required': [('recurring_task', '=', True)]}" />
|
||||
</div>
|
||||
|
||||
<label for="repeat_on_month" string="Repeat On" attrs="{'invisible': [('repeat_unit', 'not in', ('month', 'year'))]}" />
|
||||
<div class="o_row">
|
||||
<field name="repeat_on_month" attrs="{'invisible': [('repeat_unit', '!=', 'month')], 'required': [('repeat_unit', '=', 'month')]}" />
|
||||
<field name="repeat_on_year" attrs="{'invisible': [('repeat_unit', '!=', 'year')], 'required': [('repeat_unit', '=', 'year')]}" />
|
||||
|
||||
<field name="repeat_day" attrs="{'invisible': [('repeat_show_day', '=', False)], 'required': [('repeat_show_day', '=', True)]}" />
|
||||
<field name="repeat_week" attrs="{'invisible': [('repeat_show_week', '=', False)], 'required': [('repeat_show_week', '=', True)]}" />
|
||||
<field name="repeat_weekday" attrs="{'invisible': [('repeat_show_week', '=', False)], 'required': [('repeat_show_week', '=', True)]}" />
|
||||
<span attrs="{'invisible': ['|', ('repeat_show_week', '=', False), ('repeat_show_month', '=', False)]}">of</span>
|
||||
<field name="repeat_month" attrs="{'invisible': [('repeat_show_month', '=', False)], 'required': [('repeat_show_month', '=', True)]}" />
|
||||
</div>
|
||||
|
||||
|
||||
<field name="mon" attrs="{'invisible': [('repeat_show_dow', '=', False)]}" />
|
||||
<field name="tue" attrs="{'invisible': [('repeat_show_dow', '=', False)]}" />
|
||||
<field name="wed" attrs="{'invisible': [('repeat_show_dow', '=', False)]}" />
|
||||
<field name="thu" attrs="{'invisible': [('repeat_show_dow', '=', False)]}" />
|
||||
<field name="fri" attrs="{'invisible': [('repeat_show_dow', '=', False)]}" />
|
||||
<field name="sat" attrs="{'invisible': [('repeat_show_dow', '=', False)]}" />
|
||||
<field name="sun" attrs="{'invisible': [('repeat_show_dow', '=', False)]}" />
|
||||
|
||||
<label for="repeat_type" />
|
||||
<div class="o_row">
|
||||
<field name="repeat_type" attrs="{'required': [('recurring_task', '=', True)]}" />
|
||||
<field name="repeat_until" attrs="{'invisible': [('repeat_type', '!=', 'until')], 'required': [('repeat_type', '=', 'until')]}" />
|
||||
<field name="repeat_number" attrs="{'invisible': [('repeat_type', '!=', 'after')], 'required': [('repeat_type', '=', 'after')]}" />
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<group attrs="{'invisible': ['|', ('recurring_task', '=', False), ('recurrence_message', '=', False)]}">
|
||||
<div class="alert alert-success o_form_project_recurrence_message" role="status">
|
||||
<p><span class="fa fa-check-circle"></span> Next Occurrences:</p>
|
||||
<field name="recurrence_message" widget="html" class="mb-0" />
|
||||
</div>
|
||||
</group>
|
||||
</page>
|
||||
<page name="extra_info" string="Extra Info" groups="base.group_no_one">
|
||||
<group>
|
||||
<group>
|
||||
|
||||
@@ -49,6 +49,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box" id="recurring_tasks_setting">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="group_project_recurring_tasks"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="group_project_recurring_tasks"/>
|
||||
<div class="text-muted">
|
||||
Auto-generate tasks for regular activities
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Time Management</h2>
|
||||
<div class="row mt16 o_settings_container" name="project_time">
|
||||
|
||||
@@ -143,3 +143,13 @@ class ProjectTask(models.Model):
|
||||
})
|
||||
action['context'] = context
|
||||
return action
|
||||
|
||||
class ProjectTaskRecurrence(models.Model):
|
||||
_inherit = 'project.task.recurrence'
|
||||
|
||||
def _new_task_values(self, task):
|
||||
values = super(ProjectTaskRecurrence, self)._new_task_values(task)
|
||||
task = self.sudo().task_ids[0]
|
||||
if not task.is_fsm:
|
||||
values['sale_line_id'] = task.sale_line_id.id
|
||||
return values
|
||||
|
||||
@@ -309,3 +309,10 @@ class ProjectTask(models.Model):
|
||||
|
||||
def _get_action_view_so_ids(self):
|
||||
return list(set((self.sale_order_id + self.timesheet_ids.so_line.order_id).ids))
|
||||
|
||||
class ProjectTaskRecurrence(models.Model):
|
||||
_inherit = 'project.task.recurrence'
|
||||
|
||||
@api.model
|
||||
def _get_recurring_fields(self):
|
||||
return ['analytic_account_id'] + super(ProjectTaskRecurrence, self)._get_recurring_fields()
|
||||
|
||||
Reference in New Issue
Block a user