[IMP] project: link milestone to task
Before this commit, the milestones are only linked to a project without any link to the tasks of the same project. However, the tasks could be considered as steps to reach a project milestone. This commit adds a link between the `project.task` and `project.milestone` models. This link is added in the `project.task` model with the Many2One field called `milestone_id`. With this field, the user will be able to link a milestone to a task. task-2829542
This commit is contained in:
@@ -3524,7 +3524,7 @@
|
||||
</record>
|
||||
|
||||
<!-- Projects -->
|
||||
<record id="project.project_milestone_2" model="project.milestone">
|
||||
<record id="project.project_1_milestone_2" model="project.milestone">
|
||||
<field name="deadline" eval="(DateTime.now() + relativedelta(years=1, months=3)).strftime('%Y-%m-15')"/>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
'views/project_collaborator_views.xml',
|
||||
'wizard/project_report_wizard_views.xml',
|
||||
'views/project_views.xml',
|
||||
'views/project_milestone_views.xml',
|
||||
'views/res_partner_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/mail_activity_views.xml',
|
||||
|
||||
@@ -204,6 +204,27 @@
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
</record>
|
||||
|
||||
<!-- Project 1 Milestones -->
|
||||
<record id="project_1_milestone_1" model="project.milestone">
|
||||
<field name="is_reached" eval="True"/>
|
||||
<field name="deadline" eval="time.strftime('%Y-%m-10')"/>
|
||||
<field name="name">First Phase</field>
|
||||
<field name="reached_date" eval="time.strftime('%Y-%m-10')"/>
|
||||
<field name="project_id" ref="project.project_project_1"/>
|
||||
</record>
|
||||
<record id="project_1_milestone_2" model="project.milestone">
|
||||
<field name="is_reached" eval="False"/>
|
||||
<field name="deadline" eval="(DateTime.now() + relativedelta(years=1)).strftime('%Y-%m-15')"/>
|
||||
<field name="name">Second Phase</field>
|
||||
<field name="project_id" ref="project.project_project_1"/>
|
||||
</record>
|
||||
<record id="project_1_milestone_3" model="project.milestone">
|
||||
<field name="is_reached" eval="False"/>
|
||||
<field name="deadline" eval="(DateTime.now() + relativedelta(years=2)).strftime('%Y-%m-%d')"/>
|
||||
<field name="name">Final Phase</field>
|
||||
<field name="project_id" ref="project.project_project_1"/>
|
||||
</record>
|
||||
|
||||
<!-- Project 1 Tasks -->
|
||||
<record id="project_1_task_1" model="project.task">
|
||||
<field name="sequence">20</field>
|
||||
@@ -214,6 +235,7 @@
|
||||
<field name="stage_id" ref="project_stage_2"/>
|
||||
<field name="color">7</field>
|
||||
<field name="create_date" eval="DateTime.now() - relativedelta(months=5)"/>
|
||||
<field name="milestone_id" ref="project.project_1_milestone_1" />
|
||||
</record>
|
||||
|
||||
<record id="project_1_task_2" model="project.task">
|
||||
@@ -224,6 +246,7 @@
|
||||
<field name="name">Lunch Room: kitchen</field>
|
||||
<field name="stage_id" ref="project_stage_2"/>
|
||||
<field name="create_date" eval="DateTime.now() - relativedelta(months=5)"/>
|
||||
<field name="milestone_id" ref="project.project_1_milestone_1" />
|
||||
</record>
|
||||
<record id="project_1_task_2_mail_message_1" model="mail.message">
|
||||
<field name="model">project.task</field>
|
||||
@@ -273,6 +296,7 @@
|
||||
<field name="stage_id" ref="project_stage_2"/>
|
||||
<field name="color">4</field>
|
||||
<field name="create_date" eval="DateTime.now() - relativedelta(months=5)"/>
|
||||
<field name="milestone_id" ref="project.project_1_milestone_2" />
|
||||
</record>
|
||||
<record id="project_1_task_3_mail_message_1" model="mail.message">
|
||||
<field name="model">project.task</field>
|
||||
@@ -322,6 +346,7 @@
|
||||
<field name="description">Modifications to the kitchen of the lunch room</field>
|
||||
<field name="tag_ids" eval="[Command.set([ref('project_tags_00')])]"/>
|
||||
<field name="create_date" eval="DateTime.now() - relativedelta(months=5)"/>
|
||||
<field name="milestone_id" ref="project.project_1_milestone_2" />
|
||||
</record>
|
||||
<record id="project_1_task_4_mail_message_1" model="mail.message">
|
||||
<field name="model">project.task</field>
|
||||
@@ -371,6 +396,7 @@
|
||||
<field name="create_date" eval="DateTime.now() - relativedelta(months=5)"/>
|
||||
<field name="date_deadline" eval="DateTime.now() + relativedelta(days=2)"/>
|
||||
<field name="color">1</field>
|
||||
<field name="milestone_id" ref="project.project_1_milestone_3" />
|
||||
</record>
|
||||
<record id="project_1_task_5_mail_message_1" model="mail.message">
|
||||
<field name="model">project.task</field>
|
||||
@@ -413,6 +439,7 @@
|
||||
<field name="tag_ids" eval="[Command.set([ref('project_tags_01')])]"/>
|
||||
<field name="create_date" eval="DateTime.now() - relativedelta(months=5)"/>
|
||||
<field name="color">11</field>
|
||||
<field name="milestone_id" ref="project.project_1_milestone_3" />
|
||||
</record>
|
||||
<record id="project_1_task_6_mail_message_1" model="mail.message">
|
||||
<field name="model">project.task</field>
|
||||
@@ -452,6 +479,7 @@
|
||||
<field name="create_date" eval="DateTime.now() - relativedelta(months=5)"/>
|
||||
<field name="date_deadline" eval="DateTime.now() + relativedelta(days=6)"/>
|
||||
<field name="color">9</field>
|
||||
<field name="milestone_id" ref="project.project_1_milestone_3" />
|
||||
</record>
|
||||
<record id="project_1_task_7_mail_message_1" model="mail.message">
|
||||
<field name="model">project.task</field>
|
||||
@@ -485,6 +513,7 @@
|
||||
<field name="kanban_state">blocked</field>
|
||||
<field name="tag_ids" eval="[Command.set([ref('project_tags_01')])]"/>
|
||||
<field name="create_date" eval="DateTime.now() - relativedelta(months=5)"/>
|
||||
<field name="milestone_id" ref="project.project_1_milestone_3" />
|
||||
</record>
|
||||
<record id="project_1_task_8_mail_message_1" model="mail.message">
|
||||
<field name="model">project.task</field>
|
||||
@@ -533,6 +562,7 @@
|
||||
<field name="stage_id" ref="project_stage_1"/>
|
||||
<field name="color">3</field>
|
||||
<field name="create_date" eval="DateTime.now() - relativedelta(months=5)"/>
|
||||
<field name="milestone_id" ref="project.project_1_milestone_3" />
|
||||
</record>
|
||||
<record id="project_1_task_9_activity_1" model="mail.activity">
|
||||
<field name="res_id" ref="project_1_task_9"/>
|
||||
@@ -581,6 +611,7 @@
|
||||
<field name="recurring_task" eval="True"/>
|
||||
<field name="recurrence_id" ref="project_task_recurrence_1"/>
|
||||
<field name="create_date" eval="DateTime.now() + relativedelta(weeks=-2)"/>
|
||||
<field name="milestone_id" ref="project.project_1_milestone_3" />
|
||||
</record>
|
||||
<record id="project_1_task_11" model="project.task">
|
||||
<field name="sequence">20</field>
|
||||
@@ -1277,26 +1308,6 @@ Send it ASAP, its urgent.</field>
|
||||
<field name="rating_template_id" ref="rating_project_request_email_template"/>
|
||||
</record>
|
||||
|
||||
<record id="project_milestone_1" model="project.milestone">
|
||||
<field name="is_reached" eval="True"/>
|
||||
<field name="deadline" eval="time.strftime('%Y-%m-10')"/>
|
||||
<field name="name">First Phase</field>
|
||||
<field name="reached_date" eval="time.strftime('%Y-%m-10')"/>
|
||||
<field name="project_id" ref="project.project_project_1"/>
|
||||
</record>
|
||||
<record id="project_milestone_2" model="project.milestone">
|
||||
<field name="is_reached" eval="False"/>
|
||||
<field name="deadline" eval="(DateTime.now() + relativedelta(years=1)).strftime('%Y-%m-15')"/>
|
||||
<field name="name">Second Phase</field>
|
||||
<field name="project_id" ref="project.project_project_1"/>
|
||||
</record>
|
||||
<record id="project_milestone_3" model="project.milestone">
|
||||
<field name="is_reached" eval="False"/>
|
||||
<field name="deadline" eval="(DateTime.now() + relativedelta(years=2)).strftime('%Y-%m-%d')"/>
|
||||
<field name="name">Final Phase</field>
|
||||
<field name="project_id" ref="project.project_project_1"/>
|
||||
</record>
|
||||
|
||||
<record id="project_update_1" model="project.update" context="{'default_project_id': ref('project.project_project_1')}">
|
||||
<field name="name">Review of the situation</field>
|
||||
<field name="user_id" eval="ref('base.user_demo')"/>
|
||||
|
||||
@@ -323,6 +323,7 @@ class Project(models.Model):
|
||||
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'))
|
||||
allow_task_dependencies = fields.Boolean('Task Dependencies', default=lambda self: self.env.user.has_group('project.group_project_task_dependencies'))
|
||||
allow_milestones = fields.Boolean('Milestones', default=lambda self: self.env.user.has_group('project.group_project_milestone'))
|
||||
tag_ids = fields.Many2many('project.tags', relation='project_project_project_tags_rel', string='Tags')
|
||||
|
||||
# Project Sharing fields
|
||||
@@ -363,7 +364,7 @@ class Project(models.Model):
|
||||
], default='to_define', compute='_compute_last_update_status', store=True, readonly=False, required=True)
|
||||
last_update_color = fields.Integer(compute='_compute_last_update_color')
|
||||
milestone_ids = fields.One2many('project.milestone', 'project_id', copy=True)
|
||||
milestone_count = fields.Integer(compute='_compute_milestone_count')
|
||||
milestone_count = fields.Integer(compute='_compute_milestone_count', groups='project.group_project_milestone')
|
||||
is_milestone_exceeded = fields.Boolean(compute="_compute_is_milestone_exceeded", search='_search_is_milestone_exceeded')
|
||||
|
||||
_sql_constraints = [
|
||||
@@ -782,10 +783,11 @@ class Project(models.Model):
|
||||
return {}
|
||||
panel_data = {
|
||||
'user': self._get_user_values(),
|
||||
'milestones': self._get_milestones(),
|
||||
'buttons': sorted(self._get_stat_buttons(), key=lambda k: k['sequence']),
|
||||
'currency_id': self.currency_id.id,
|
||||
}
|
||||
if self.allow_milestones:
|
||||
panel_data['milestones'] = self._get_milestones()
|
||||
if self._show_profitability():
|
||||
profitability_items = self._get_profitability_items()
|
||||
if self._get_profitability_sequence_per_invoice_type() and profitability_items and 'revenues' in profitability_items and 'costs' in profitability_items: # sort the data values
|
||||
@@ -1120,6 +1122,19 @@ 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'])])
|
||||
is_private = fields.Boolean(compute='_compute_is_private')
|
||||
allow_milestones = fields.Boolean(related='project_id.allow_milestones')
|
||||
milestone_id = fields.Many2one(
|
||||
'project.milestone',
|
||||
'Milestone',
|
||||
domain="[('project_id', '=', project_id)]",
|
||||
compute='_compute_milestone_id',
|
||||
readonly=False,
|
||||
store=True,
|
||||
tracking=True,
|
||||
help="Track major progress points that must be reached to achieve success (e.g. Product Launch). "
|
||||
"After all the tasks connected to this milestone are completed, you will be invited to mark it as reached if you wish. "
|
||||
"You can deliver your services automatically when a milestone is reached by linking it to a sales order item."
|
||||
)
|
||||
|
||||
# Task Dependencies fields
|
||||
allow_task_dependencies = fields.Boolean(related='project_id.allow_task_dependencies')
|
||||
@@ -1994,6 +2009,12 @@ class Task(models.Model):
|
||||
if task.parent_id:
|
||||
task.project_id = task.display_project_id or task.parent_id.project_id
|
||||
|
||||
@api.depends('project_id')
|
||||
def _compute_milestone_id(self):
|
||||
for task in self:
|
||||
if task.project_id != task.milestone_id.project_id:
|
||||
task.milestone_id = False
|
||||
|
||||
# ---------------------------------------------------
|
||||
# Mail gateway
|
||||
# ---------------------------------------------------
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo import api, fields, models
|
||||
|
||||
class ProjectMilestone(models.Model):
|
||||
_name = 'project.milestone'
|
||||
@@ -17,10 +17,12 @@ class ProjectMilestone(models.Model):
|
||||
deadline = fields.Date(tracking=True, copy=False)
|
||||
is_reached = fields.Boolean(string="Reached", default=False, copy=False)
|
||||
reached_date = fields.Date(compute='_compute_reached_date', store=True)
|
||||
task_ids = fields.One2many('project.task', 'milestone_id', 'Tasks')
|
||||
|
||||
# computed non-stored fields
|
||||
is_deadline_exceeded = fields.Boolean(compute="_compute_is_deadline_exceeded")
|
||||
is_deadline_future = fields.Boolean(compute="_compute_is_deadline_future")
|
||||
task_count = fields.Integer('# of Tasks', compute='_compute_task_count', groups='project.group_project_milestone')
|
||||
|
||||
@api.depends('is_reached')
|
||||
def _compute_reached_date(self):
|
||||
@@ -38,11 +40,29 @@ class ProjectMilestone(models.Model):
|
||||
for ms in self:
|
||||
ms.is_deadline_future = ms.deadline and ms.deadline > fields.Date.context_today(self)
|
||||
|
||||
@api.depends('task_ids.milestone_id')
|
||||
def _compute_task_count(self):
|
||||
task_read_group = self.env['project.task']._read_group([('milestone_id', 'in', self.ids), ('allow_milestones', '=', True)], ['milestone_id'], ['milestone_id'])
|
||||
task_count_per_milestone = {res['milestone_id'][0]: res['milestone_id_count'] for res in task_read_group}
|
||||
for milestone in self:
|
||||
milestone.task_count = task_count_per_milestone.get(milestone.id, 0)
|
||||
|
||||
def toggle_is_reached(self, is_reached):
|
||||
self.ensure_one()
|
||||
self.update({'is_reached': is_reached})
|
||||
return self._get_data()
|
||||
|
||||
def action_view_tasks(self):
|
||||
self.ensure_one()
|
||||
action = self.env['ir.actions.act_window']._for_xml_id('project.action_view_task_from_milestone')
|
||||
action['context'] = {'default_project_id': self.project_id.id, 'default_milestone_id': self.id}
|
||||
if self.task_count == 1:
|
||||
action['view_mode'] = 'form'
|
||||
action['res_id'] = self.task_ids.id
|
||||
if 'views' in action:
|
||||
action['views'] = [(view_id, view_type) for view_id, view_type in action['views'] if view_type == 'form']
|
||||
return action
|
||||
|
||||
@api.model
|
||||
def _get_fields_to_export(self):
|
||||
return ['id', 'name', 'deadline', 'is_reached', 'reached_date', 'is_deadline_exceeded', 'is_deadline_future']
|
||||
|
||||
@@ -110,6 +110,14 @@ class ProjectUpdate(models.Model):
|
||||
@api.model
|
||||
def _get_milestone_values(self, project):
|
||||
Milestone = self.env['project.milestone']
|
||||
if not project.allow_milestones:
|
||||
return {
|
||||
'show_section': False,
|
||||
'list': [],
|
||||
'updated': [],
|
||||
'last_update_date': None,
|
||||
'created': []
|
||||
}
|
||||
list_milestones = Milestone.search(
|
||||
[('project_id', '=', project.id),
|
||||
'|', ('deadline', '<', fields.Date.context_today(self) + relativedelta(years=1)), ('deadline', '=', False)])._get_data_list()
|
||||
|
||||
@@ -14,6 +14,7 @@ class ResConfigSettings(models.TransientModel):
|
||||
group_project_stages = fields.Boolean("Project Stages", implied_group="project.group_project_stages")
|
||||
group_project_recurring_tasks = fields.Boolean("Recurring Tasks", implied_group="project.group_project_recurring_tasks")
|
||||
group_project_task_dependencies = fields.Boolean("Task Dependencies", implied_group="project.group_project_task_dependencies")
|
||||
group_project_milestone = fields.Boolean('Milestones', implied_group='project.group_project_milestone')
|
||||
rating_status = fields.Selection(
|
||||
[('stage', 'Rating when changing stage'),
|
||||
('periodic', 'Periodic rating')
|
||||
@@ -41,6 +42,7 @@ class ResConfigSettings(models.TransientModel):
|
||||
basic_project_features = {
|
||||
"group_subtask_project": "allow_subtasks",
|
||||
"group_project_task_dependencies": "allow_task_dependencies",
|
||||
'group_project_milestone': 'allow_milestones',
|
||||
}
|
||||
config_feature_vals = {config_flag: self[config_flag]
|
||||
for config_flag in global_features.keys() | basic_project_features.keys()}
|
||||
|
||||
@@ -44,6 +44,11 @@
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
</record>
|
||||
|
||||
<record id="group_project_milestone" model="res.groups">
|
||||
<field name="name">Use Milestones</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'))]"/>
|
||||
|
||||
@@ -197,8 +197,9 @@
|
||||
</t>
|
||||
|
||||
<t t-name="project.MilestoneSection" owl="1">
|
||||
<t t-set="show_milestones" t-value="state.data.hasOwnProperty('milestones')"/>
|
||||
<t t-set="milestones" t-value="state.data.milestones"/>
|
||||
<div class="o_rightpanel_section" name="milestone">
|
||||
<div class="o_rightpanel_section" name="milestone" t-if="show_milestones">
|
||||
<div class="o_rightpanel_header">
|
||||
<div class="o_rightpanel_title o_cursor_default">
|
||||
Milestones
|
||||
|
||||
@@ -5,6 +5,7 @@ from . import test_burndown_chart
|
||||
from . import test_project_base
|
||||
from . import test_project_config
|
||||
from . import test_project_flow
|
||||
from . import test_project_milestone
|
||||
from . import test_project_profitability
|
||||
from . import test_project_recurrence
|
||||
from . import test_project_sharing
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import Form, tagged
|
||||
|
||||
from .test_project_base import TestProjectCommon
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestProjectMilestone(TestProjectCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.milestone = cls.env['project.milestone'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Test Milestone',
|
||||
'project_id': cls.project_pigs.id,
|
||||
})
|
||||
|
||||
def test_milestones_settings_change(self):
|
||||
# To be sure the feature is disabled globally to begin the test.
|
||||
self.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': False}) \
|
||||
.execute()
|
||||
self.assertFalse(self.env.user.has_group('project.group_project_milestone'), 'The "Milestones" feature should not be globally enabled by default.')
|
||||
self.assertFalse(self.project_pigs.allow_milestones, 'The "Milestones" feature should not be enabled by default.')
|
||||
self.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': True}) \
|
||||
.execute()
|
||||
self.assertTrue(self.env.user.has_group('project.group_project_milestone'), 'The "Milestones" feature should globally be enabled.')
|
||||
self.assertTrue(self.project_pigs.allow_milestones, 'The "Milestones" feature should be enabled by default on the project when the feature is enabled.')
|
||||
project = self.env['project.project'].create({'name': 'Test allow_milestones on New Project'})
|
||||
self.assertTrue(project.allow_milestones, 'The "Milestones" feature should be enabled by default when the feature is enabled globally.')
|
||||
|
||||
with Form(self.env['project.project']) as project_form:
|
||||
project_form.name = 'My Mouses Project'
|
||||
self.assertTrue(project_form.allow_milestones, 'New projects allow_milestones should be True by default.')
|
||||
|
||||
def test_change_project_in_task(self):
|
||||
""" Test when a task is linked to a milestone and when we change its project the milestone is removed
|
||||
|
||||
Test Case:
|
||||
=========
|
||||
1) Set a milestone on the task
|
||||
2) Change the project of that task
|
||||
3) Check no milestone is linked to the task
|
||||
"""
|
||||
self.task_1.milestone_id = self.milestone
|
||||
self.assertEqual(self.task_1.milestone_id, self.milestone)
|
||||
|
||||
self.task_1.project_id = self.project_goats
|
||||
self.assertFalse(self.task_1.milestone_id, 'No milestone should be linked to the task since its project has changed')
|
||||
@@ -10,6 +10,12 @@ from odoo.addons.project.tests.test_project_base import TestProjectCommon
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestProjectUpdate(TestProjectCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': True}) \
|
||||
.execute()
|
||||
|
||||
def test_project_update_form(self):
|
||||
with Form(self.env['project.milestone'].with_context({'default_project_id': self.project_pigs.id})) as milestone_form:
|
||||
@@ -47,9 +53,29 @@ class TestProjectUpdate(TestProjectCommon):
|
||||
|
||||
template_values = self.env['project.update']._get_template_values(self.project_pigs)
|
||||
|
||||
self.assertTrue(template_values['milestones']['show_section'], 'The milestone section should not be visible since the feature is disabled')
|
||||
self.assertEqual(len(template_values['milestones']['list']), 2, "Milestone list length should be equal to 2")
|
||||
self.assertEqual(len(template_values['milestones']['created']), 3, "Milestone created length tasks should be equal to 3")
|
||||
|
||||
self.project_pigs.write({'allow_milestones': False})
|
||||
|
||||
template_values = self.env['project.update']._get_template_values(self.project_pigs)
|
||||
|
||||
self.assertFalse(template_values['milestones']['show_section'], 'The milestone section should not be visible since the feature is disabled')
|
||||
self.assertEqual(len(template_values['milestones']['list']), 0, "Milestone list length should be equal to 0 because the Milestones feature is disabled.")
|
||||
self.assertEqual(len(template_values['milestones']['created']), 0, "Milestone created length tasks should be equal to 0 because the Milestones feature is disabled.")
|
||||
|
||||
self.project_pigs.write({'allow_milestones': True})
|
||||
self.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': False}) \
|
||||
.execute()
|
||||
|
||||
template_values = self.env['project.update']._get_template_values(self.project_pigs)
|
||||
|
||||
self.assertFalse(template_values['milestones']['show_section'], 'The milestone section should not be visible since the feature is disabled')
|
||||
self.assertEqual(len(template_values['milestones']['list']), 0, "Milestone list length should be equal to 0 because the Milestones feature is disabled.")
|
||||
self.assertEqual(len(template_values['milestones']['created']), 0, "Milestone created length tasks should be equal to 0 because the Milestones feature is disabled.")
|
||||
|
||||
def test_project_update_panel(self):
|
||||
with Form(self.env['project.milestone'].with_context({'default_project_id': self.project_pigs.id})) as milestone_form:
|
||||
milestone_form.name = "Test 1"
|
||||
@@ -73,3 +99,16 @@ class TestProjectUpdate(TestProjectCommon):
|
||||
self.assertEqual(panel_data['milestones']['data'][0]['name'], "Test 2", "Sorting isn't correct")
|
||||
self.assertEqual(panel_data['milestones']['data'][1]['name'], "Test 1", "Sorting isn't correct")
|
||||
self.assertEqual(panel_data['milestones']['data'][2]['name'], "Test 3", "Sorting isn't correct")
|
||||
|
||||
# Disable the "Milestones" feature in the project and check the "Milestones" section is not loaded for this project.
|
||||
self.project_pigs.write({'allow_milestones': False})
|
||||
panel_data = self.project_pigs.get_panel_data()
|
||||
self.assertNotIn('milestones', panel_data, 'Since the "Milestones" feature is disabled in this project, the "Milestones" section is not loaded.')
|
||||
|
||||
# Disable globally the Milestones feature and check the Milestones section is not loaded.
|
||||
self.project_pigs.write({'allow_milestones': True})
|
||||
self.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': False}) \
|
||||
.execute()
|
||||
panel_data = self.project_pigs.get_panel_data()
|
||||
self.assertNotIn('milestones', panel_data, 'Since the "Milestones" feature is globally disabled, the "Milestones" section is not loaded.')
|
||||
|
||||
@@ -4,6 +4,14 @@ from odoo.tests import HttpCase, tagged
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestProjectUpdateUi(HttpCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# Enable the "Milestones" feature to be able to create milestones on this tour.
|
||||
cls.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': True}) \
|
||||
.execute()
|
||||
|
||||
def test_01_project_tour(self):
|
||||
self.start_tour("/web", 'project_update_tour', login="admin")
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="project_milestone_view_form" model="ir.ui.view">
|
||||
<field name="name">project.milestone.view.form</field>
|
||||
<field name="model">project.milestone</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="%(project.action_view_task_from_milestone)d"
|
||||
type="action"
|
||||
class="oe_stat_button"
|
||||
icon="fa-tasks"
|
||||
attrs="{'invisible': [('task_count', '=', 0)]}"
|
||||
context="{'default_project_id': project_id}"
|
||||
groups="project.group_project_milestone"
|
||||
>
|
||||
<field name="task_count" string="Tasks" widget="statinfo"/>
|
||||
</button>
|
||||
</div>
|
||||
<group>
|
||||
<group name="main_details">
|
||||
<field name="project_id" invisible="1"/>
|
||||
<field name="name" placeholder="e.g: Product Launch"/>
|
||||
<field name="deadline"/>
|
||||
<field name="is_reached"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_milestone_view_tree" model="ir.ui.view">
|
||||
<field name="name">project.milestone.view.tree</field>
|
||||
<field name="model">project.milestone</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree decoration-danger="is_deadline_exceeded" decoration-muted="is_reached" editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="deadline" optional="show"/>
|
||||
<field name="is_reached" optional="show"/>
|
||||
<field name="is_deadline_exceeded" invisible="1"/>
|
||||
<field name="task_count" invisible="1" />
|
||||
<button name="action_view_tasks"
|
||||
type="object"
|
||||
title="View Tasks"
|
||||
string="View Tasks"
|
||||
class="btn btn-link float-end"
|
||||
attrs="{'invisible': [('task_count', '=', 0)]}"
|
||||
groups="project.group_project_milestone"
|
||||
/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -119,35 +119,4 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_milestone_view_form" model="ir.ui.view">
|
||||
<field name="name">project.milestone.view.form</field>
|
||||
<field name="model">project.milestone</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<group name="main_details">
|
||||
<field name="project_id" invisible="1"/>
|
||||
<field name="name" placeholder="e.g. Product Launch"/>
|
||||
<field name="deadline"/>
|
||||
<field name="is_reached"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_milestone_view_tree" model="ir.ui.view">
|
||||
<field name="name">project.milestone.view.tree</field>
|
||||
<field name="model">project.milestone</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree decoration-danger="is_deadline_exceeded" decoration-muted="is_reached" editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="deadline" optional="show"/>
|
||||
<field name="is_reached" optional="show"/>
|
||||
<field name="is_deadline_exceeded" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<field name="tag_ids"/>
|
||||
<field name="user_ids" filter_domain="[('user_ids', 'ilike', self), ('user_ids.active', 'in', [True, False])]"/>
|
||||
<field string="Project" name="display_project_id"/>
|
||||
<field name="milestone_id" groups="project.group_project_milestone"/>
|
||||
<field name="stage_id"/>
|
||||
<field name="partner_id" operator="child_of"/>
|
||||
<field name="parent_id"/>
|
||||
@@ -58,6 +59,7 @@
|
||||
<filter string="Personal Stage" name="personal_stage" context="{'group_by': 'personal_stage_type_ids'}"/>
|
||||
<filter string="Assignees" name="user" context="{'group_by': 'user_ids'}"/>
|
||||
<filter string="Project" name="project" context="{'group_by': 'project_id'}"/>
|
||||
<filter string="Milestone" name="milestone" context="{'group_by': 'milestone_id'}" groups="project.group_project_milestone"/>
|
||||
<filter string="Customer" name="customer" context="{'group_by': 'partner_id'}"/>
|
||||
<filter string="Kanban State" name="kanban_state" context="{'group_by': 'kanban_state'}"/>
|
||||
<filter string="Deadline" name="date_deadline" context="{'group_by': 'date_deadline'}"/>
|
||||
@@ -415,7 +417,7 @@
|
||||
name="%(act_project_project_2_project_task_all)d" icon="fa-tasks">
|
||||
<field string="Tasks In Progress" name="task_count" widget="statinfo" options="{'label_field': 'label_tasks'}"/>
|
||||
</button>
|
||||
<button class="oe_stat_button" name="action_get_list_view" type="object" icon="fa-check-square-o">
|
||||
<button class="oe_stat_button" name="action_get_list_view" type="object" icon="fa-check-square-o" attrs="{'invisible': [('allow_milestones', '=', False)]}">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="milestone_count" nolabel="1"/>
|
||||
@@ -580,6 +582,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 o_setting_box" id="project_milestone_setting" groups="project.group_project_milestone">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="allow_milestones"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="allow_milestones"/>
|
||||
<div class="text-muted">
|
||||
Track major progress points that must be reached to achieve success
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</page>
|
||||
</notebook>
|
||||
@@ -609,7 +622,7 @@
|
||||
<filter string="Followed" name="followed_by_me" domain="[('message_is_follower', '=', True)]"/>
|
||||
<filter string="Unassigned" name="unassigned_projects" domain="[('user_id', '=', False)]"/>
|
||||
<separator/>
|
||||
<filter string="Late Milestones" name="late_milestones" domain="[('is_milestone_exceeded', '=', True)]"/>
|
||||
<filter string="Late Milestones" name="late_milestones" domain="[('is_milestone_exceeded', '=', True)]" groups="project.group_project_milestone"/>
|
||||
<separator/>
|
||||
<filter string="Open" name="open_project" domain="[('stage_id.fold', '=', False)]" groups="project.group_project_stages"/>
|
||||
<filter string="Closed" name="closed_project" domain="[('stage_id.fold', '=', True)]" groups="project.group_project_stages"/>
|
||||
@@ -801,6 +814,9 @@
|
||||
<div role="menuitem">
|
||||
<a name="action_view_tasks" type="object">Tasks</a>
|
||||
</div>
|
||||
<div role="menuitem">
|
||||
<a name="action_get_list_view" type="object">Milestones</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 o_kanban_card_manage_section o_kanban_manage_reporting">
|
||||
<div role="menuitem" class="o_kanban_card_manage_title" groups="project.group_project_user">
|
||||
@@ -1005,6 +1021,7 @@
|
||||
<field name="allow_task_dependencies" invisible="1" />
|
||||
<field name="rating_last_value" invisible="1"/>
|
||||
<field name="rating_count" invisible="1"/>
|
||||
<field name="allow_milestones" invisible="1" />
|
||||
<header>
|
||||
<button name="action_assign_to_me" string="Assign to Me" type="object" attrs="{'invisible': "['|', ('user_ids', 'in', uid), ('user_ids', '=', [])]"}" data-hotkey="q"/>
|
||||
<button name="action_assign_to_me" string="Assign to Me" type="object" class="oe_highlight" attrs="{'invisible' : "['|', ('user_ids', 'in', uid), ('user_ids', '!=', [])]"}" data-hotkey="q"/>
|
||||
@@ -1084,6 +1101,11 @@
|
||||
<field name="project_id" attrs="{'invisible': [('parent_id', '!=', False)]}" domain="[('active', '=', True), ('company_id', '=', company_id)]"
|
||||
placeholder="Private" class="o_project_task_project_field oe_edit_only"/>
|
||||
<field name="display_project_id" string="Project" attrs="{'invisible': [('parent_id', '=', False)]}" domain="[('active', '=', True), ('company_id', '=', company_id)]"/>
|
||||
<field name="milestone_id"
|
||||
placeholder="e.g. Product Launch"
|
||||
context="{'default_project_id': project_id if not parent_id or not display_project_id else display_project_id}"
|
||||
attrs="{'invisible': ['|', ('project_id', '=', False), ('allow_milestones', '=', False)]}"
|
||||
/>
|
||||
<field name="user_ids"
|
||||
class="o_task_user_field"
|
||||
options="{'no_open': True, 'no_quick_create': True}"
|
||||
@@ -1107,16 +1129,22 @@
|
||||
<field name="description" type="html" options="{'collaborative': true, 'resizable': false}"/>
|
||||
</page>
|
||||
<page name="sub_tasks_page" string="Sub-tasks" attrs="{'invisible': [('allow_subtasks', '=', False)]}">
|
||||
<field name="child_ids" context="{'default_project_id': project_id if not parent_id or not display_project_id else display_project_id, 'default_user_ids': user_ids, 'default_parent_id': id, 'default_partner_id': partner_id}" widget="subtasks_one2many">
|
||||
<field name="child_ids" context="{'default_project_id': project_id if not parent_id or not display_project_id else display_project_id, 'default_user_ids': user_ids, 'default_parent_id': id, 'default_partner_id': partner_id, 'default_milestone_id': allow_milestones and milestone_id}" widget="subtasks_one2many">
|
||||
<tree editable="bottom">
|
||||
<field name="legend_normal" invisible="1"/>
|
||||
<field name="legend_done" invisible="1"/>
|
||||
<field name="legend_blocked" invisible="1"/>
|
||||
<field name="project_id" invisible="1"/>
|
||||
<field name="is_closed" invisible="1"/>
|
||||
<field name="allow_milestones" invisible="1"/>
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="display_project_id" string="Project" optional="hide"/>
|
||||
<field name="milestone_id"
|
||||
optional="hide"
|
||||
context="{'default_project_id': display_project_id or project_id}"
|
||||
attrs="{'invisible': [('allow_milestones', '=', False)], 'column_invisible': [('parent.allow_milestones', '=', False)]}"
|
||||
/>
|
||||
<field name="partner_id" optional="hide"/>
|
||||
<field name="user_ids" widget="many2many_avatar_user" optional="show" domain="[('share', '=', False), ('active', '=', True)]"/>
|
||||
<field name="company_id" groups="base.group_multi_company" optional="hide"/>
|
||||
@@ -1132,9 +1160,17 @@
|
||||
<page name="task_dependencies" string="Blocked By" attrs="{'invisible': [('allow_task_dependencies', '=', False)]}" groups="project.group_project_task_dependencies">
|
||||
<field name="depend_on_ids" nolabel="1" context="{'default_project_id' : project_id}">
|
||||
<tree editable="bottom">
|
||||
<field name="allow_milestones" invisible="1"/>
|
||||
<field name="parent_id" invisible="1" />
|
||||
<field name="display_project_id" invisible="1" />
|
||||
<field name="is_closed" invisible="1" />
|
||||
<field name="name" />
|
||||
<field name="project_id" optional="hide" />
|
||||
<field name="milestone_id"
|
||||
optional="hide"
|
||||
context="{'default_project_id': project_id if not parent_id or not display_project_id else display_project_id}"
|
||||
attrs="{'invisible': [('allow_milestones', '=', False)], 'column_invisible': [('parent.allow_milestones', '=', False)]}"
|
||||
/>
|
||||
<field name="user_ids" widget="many2many_avatar_user" optional="show" domain="[('share', '=', False), ('active', '=', True)]"/>
|
||||
<field name="company_id" optional="hide" groups="base.group_multi_company" />
|
||||
<field name="activity_ids" widget="list_activity" optional="hide"/>
|
||||
@@ -1285,6 +1321,7 @@
|
||||
<field name="child_text"/>
|
||||
<field name="is_private"/>
|
||||
<field name="rating_active"/>
|
||||
<field name="allow_milestones" />
|
||||
<progressbar field="kanban_state" colors='{"done": "success", "blocked": "danger", "normal": "muted"}'/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
@@ -1298,6 +1335,10 @@
|
||||
</strong>
|
||||
<t t-if="!record.is_private.raw_value">
|
||||
<span invisible="context.get('default_project_id', False)"><br/><field name="project_id" required="1"/></span>
|
||||
<span t-if="record.allow_milestones.raw_value and record.milestone_id.raw_value">
|
||||
<br/>
|
||||
<field name="milestone_id" />
|
||||
</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<br/>
|
||||
@@ -1373,11 +1414,13 @@
|
||||
<field name="is_closed" invisible="1" />
|
||||
<field name="allow_subtasks" invisible="1" />
|
||||
<field name="sequence" invisible="1" readonly="1"/>
|
||||
<field name="allow_milestones" invisible="1"/>
|
||||
<field name="priority" widget="priority" optional="show" nolabel="1"/>
|
||||
<field name="id" optional="hide"/>
|
||||
<field name="name" widget="name_with_subtask_count"/>
|
||||
<field name="child_text" invisible="1"/>
|
||||
<field name="project_id" widget="project_private_task" optional="show" readonly="1"/>
|
||||
<field name="milestone_id" attrs="{'invisible': [('allow_milestones', '=', False)]}" context="{'default_project_id': project_id}" groups="project.group_project_milestone"/>
|
||||
<field name="partner_id" optional="hide"/>
|
||||
<field name="parent_id" optional="hide" attrs="{'invisible': [('allow_subtasks', '=', False)]}" groups="base.group_no_one"/>
|
||||
<field name="user_ids" optional="show" widget="many2many_avatar_user" domain="[('share', '=', False), ('active', '=', True)]"/>
|
||||
@@ -1418,12 +1461,14 @@
|
||||
<field name="model">project.task</field>
|
||||
<field eval="2" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<calendar date_start="date_deadline" string="Tasks" mode="month"
|
||||
color="color" event_limit="5" hide_time="true"
|
||||
<calendar date_start="date_deadline" string="Tasks" mode="month"
|
||||
color="color" event_limit="5" hide_time="true"
|
||||
event_open_popup="true" quick_add="false" show_unusual_days="True"
|
||||
js_class="project_calendar"
|
||||
scales="month,year">
|
||||
<field name="allow_milestones" invisible="1" />
|
||||
<field name="project_id" widget="project_private_task"/>
|
||||
<field name="milestone_id" attrs="{'invisible': [('allow_milestones', '=', False)]}"/>
|
||||
<field name="user_ids" widget="many2many_avatar_user"/>
|
||||
<field name="partner_id" attrs="{'invisible': [('partner_id', '=', False)]}"/>
|
||||
<field name="priority" widget="priority"/>
|
||||
@@ -1581,6 +1626,24 @@
|
||||
<field name="context">{'project_id':active_id}</field>
|
||||
</record>
|
||||
|
||||
<record id="action_view_task_from_milestone" model="ir.actions.act_window">
|
||||
<field name="name">Tasks</field>
|
||||
<field name="res_model">project.task</field>
|
||||
<field name="view_mode">kanban,tree,calendar,pivot,graph,activity,form</field>
|
||||
<field name="context">{'default_milestone_id': active_id}</field>
|
||||
<field name="domain">[('milestone_id', '=', active_id)]</field>
|
||||
<field name="search_view_id" ref="view_task_search_form"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No tasks found. Let's create one!
|
||||
</p>
|
||||
<p>
|
||||
Keep track of the progress of your tasks from creation to completion.<br/>
|
||||
Collaborate efficiently by chatting in real-time or via email.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu item for project -->
|
||||
<menuitem id="menu_tasks_config" name="GTD" parent="menu_project_config" sequence="2"/>
|
||||
|
||||
|
||||
@@ -59,6 +59,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box" id="project_milestone">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="group_project_milestone"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="group_project_milestone"/>
|
||||
<div class="text-muted">
|
||||
Track major progress points that must be reached to achieve success
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Time Management</h2>
|
||||
<div class="row mt16 o_settings_container" name="project_time">
|
||||
|
||||
@@ -258,6 +258,11 @@
|
||||
</record>
|
||||
|
||||
<!-- AGR Milestones -->
|
||||
<record id="agr_milestone_0" model="project.milestone">
|
||||
<field name="name">Preparation and Delivery Phase</field>
|
||||
<field name="project_id" search="[('sale_line_id', '=', ref('sale_line_13'))]"/>
|
||||
<field name="deadline" eval="(datetime.now() + relativedelta(weekday=0, weeks=-1)).strftime('%Y-%m-%d')"/>
|
||||
</record>
|
||||
<record id="agr_milestone_1" model="project.milestone">
|
||||
<field name="name">Cabinets</field>
|
||||
<field name="project_id" search="[('sale_line_id', '=', ref('sale_line_13'))]"/>
|
||||
@@ -291,6 +296,7 @@
|
||||
<field name="name">Kitchen Assembly</field>
|
||||
<field name="sale_line_id" ref="sale_line_12"/>
|
||||
<field name="project_id" search="[('sale_line_id', '=', ref('sale_line_13'))]"/>
|
||||
<field name="milestone_id" ref="agr_milestone_1" />
|
||||
<field name="user_ids" eval="[Command.link(ref('base.user_admin'))]"/>
|
||||
</record>
|
||||
|
||||
@@ -814,6 +820,7 @@
|
||||
<field name="stage_id" ref="project.project_stage_2"/>
|
||||
<field name="partner_id" ref="base.res_partner_2"/>
|
||||
<field name="user_ids" eval="[Command.link(ref('base.user_admin'))]"/>
|
||||
<field name="milestone_id" ref="sale_timesheet.agr_milestone_0" />
|
||||
</record>
|
||||
|
||||
<record id="project_agr_task_3" model="project.task">
|
||||
@@ -824,6 +831,7 @@
|
||||
<field name="stage_id" ref="project.project_stage_1"/>
|
||||
<field name="partner_id" ref="base.res_partner_2"/>
|
||||
<field name="user_ids" eval="[Command.link(ref('base.user_admin'))]"/>
|
||||
<field name="milestone_id" ref="sale_timesheet.agr_milestone_0" />
|
||||
</record>
|
||||
|
||||
<record id="project_agr_task_4" model="project.task">
|
||||
@@ -835,6 +843,7 @@
|
||||
<field name="sale_order_id" ref="sale_timesheet.sale_order_1"/>
|
||||
<field name="partner_id" ref="base.res_partner_2"/>
|
||||
<field name="user_ids" eval="[Command.link(ref('base.user_admin'))]"/>
|
||||
<field name="milestone_id" ref="sale_timesheet.agr_milestone_0" />
|
||||
</record>
|
||||
|
||||
<!-- SO2 project tasks -->
|
||||
|
||||
@@ -29,5 +29,10 @@ class TestUi(HttpCase):
|
||||
'service_tracking': 'no',
|
||||
})
|
||||
|
||||
# Enable the "Milestones" feature to be able to create milestones on this tour.
|
||||
cls.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': True}) \
|
||||
.execute()
|
||||
|
||||
def test_ui(self):
|
||||
self.start_tour('/web', 'sale_timesheet_tour', login='admin', timeout=100)
|
||||
|
||||
Reference in New Issue
Block a user