[IMP] sale_(project): generic UX improvement for project

Purpose of the commit is to improve the generic UX of task
and project views.

task-2590412

closes odoo/odoo#75509

Related: odoo/enterprise#20423
Signed-off-by: Laurent Stukkens (ltu) <ltu@odoo.com>
This commit is contained in:
Anjali
2021-11-30 14:30:43 +00:00
committed by Barad Mahendra
parent d8072e57ea
commit 86f5ca73d9
11 changed files with 220 additions and 28 deletions
+26 -7
View File
@@ -48,6 +48,7 @@ class Project(models.Model):
remaining_hours = fields.Float(compute='_compute_remaining_hours', string='Remaining Invoiced Time', compute_sudo=True)
has_planned_hours_tasks = fields.Boolean(compute='_compute_remaining_hours', compute_sudo=True,
help="True if any of the project's task has a set planned hours")
is_project_overtime = fields.Boolean('Project in Overtime', compute='_compute_remaining_hours', search='_search_is_project_overtime', compute_sudo=True)
def _compute_encode_uom_in_days(self):
self.encode_uom_in_days = self.env.company.timesheet_encode_uom_id == self.env.ref('uom.product_uom_day')
@@ -88,13 +89,31 @@ class Project(models.Model):
fields=['planned_hours:sum', 'remaining_hours:sum'], groupby='project_id')
group_per_project_id = {group['project_id'][0]: group for group in group_read}
for project in self:
group = group_per_project_id.get(project.id)
if group:
project.remaining_hours = group.get('remaining_hours')
project.has_planned_hours_tasks = bool(group.get('planned_hours'))
else:
project.remaining_hours = 0
project.has_planned_hours_tasks = False
group = group_per_project_id.get(project.id, {})
project.remaining_hours = group.get('remaining_hours', 0)
project.has_planned_hours_tasks = bool(group.get('planned_hours', False))
project.is_project_overtime = group.get('remaining_hours', 0) < 0
@api.model
def _search_is_project_overtime(self, operator, value):
if not isinstance(value, bool):
raise ValueError(_('Invalid value: %s') % value)
if operator not in ['=', '!=']:
raise ValueError(_('Invalid operator: %s') % operator)
query = """
SELECT P.id
FROM project_project P
LEFT JOIN project_task T ON P.id = T.project_id
WHERE T.planned_hours IS NOT NULL
GROUP BY P.id
HAVING SUM(T.remaining_hours) < 0
"""
if (operator == '=' and value is True) or (operator == '!=' and value is False):
operator_new = 'inselect'
else:
operator_new = 'not inselect'
return [('id', operator_new, (query, ()))]
@api.constrains('allow_timesheets', 'analytic_account_id')
def _check_allow_timesheet(self):
+15 -1
View File
@@ -212,7 +212,7 @@
</span>
<field name="effective_hours" widget="timesheet_uom" nolabel="1"/>
<button name="action_view_subtask_timesheet" type="object" class="o_form_subtask_button oe_inline oe_link" attrs="{'invisible' : ['|', ('allow_subtasks', '=', False), ('subtask_effective_hours', '=', 0.0)]}">
<button name="action_view_subtask_timesheet" type="object" class="o_form_subtask_button oe_inline oe_link mb-2" attrs="{'invisible' : ['|', ('allow_subtasks', '=', False), ('subtask_effective_hours', '=', 0.0)]}">
<span class="text-nowrap" attrs="{'invisible' : [('encode_uom_in_days', '=', True)]}">Hours Spent on Sub-tasks:</span>
<span class="text-nowrap" attrs="{'invisible' : [('encode_uom_in_days', '=', False)]}">Days Spent on Sub-tasks:</span>
</button>
@@ -340,6 +340,20 @@
</field>
</record>
<record id="view_project_project_filter_inherit_timesheet" model="ir.ui.view">
<field name="name">project.project.view.inherit.timesheet</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.view_project_project_filter"/>
<field name="arch" type="xml">
<filter name="followed_by_me" position='before'>
<filter string="My Team's Projects" name="my_team_projects" domain="[('user_id.employee_parent_id.user_id', '=', uid)]"/>
</filter>
<filter name="late_milestones" position="before">
<filter string="Projects in Overtime" name="projects_in_overtime" domain="[('is_project_overtime', '=', True)]"/>
</filter>
</field>
</record>
<record id="project_task_view_graph" model="ir.ui.view">
<field name="name">project.task.view.graph.inherited</field>
<field name="model">project.task</field>
+1
View File
@@ -35,6 +35,7 @@
'views/project_project_stage_views.xml',
'wizard/project_share_wizard_views.xml',
'views/project_collaborator_views.xml',
'wizard/project_report_wizard_views.xml',
'views/project_views.xml',
'views/res_partner_views.xml',
'views/res_config_settings_views.xml',
+33 -1
View File
@@ -310,6 +310,7 @@ class Project(models.Model):
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')
is_milestone_exceeded = fields.Boolean(compute="_compute_is_milestone_exceeded", search='_search_is_milestone_exceeded')
_sql_constraints = [
('project_date_greater', 'check(date >= date_start)', 'Error! Project start date must be before project end date.')
@@ -380,6 +381,37 @@ class Project(models.Model):
for project in self:
project.milestone_count = mapped_count.get(project.id, 0)
@api.depends('milestone_ids', 'milestone_ids.is_reached', 'milestone_ids.deadline')
def _compute_is_milestone_exceeded(self):
today = fields.Date.context_today(self)
read_group = self.env['project.milestone'].read_group([
('project_id', 'in', self.ids),
('is_reached', '=', False),
('deadline', '<', today)], ['project_id'], ['project_id'])
mapped_count = {group['project_id'][0]: group['project_id_count'] for group in read_group}
for project in self:
project.is_milestone_exceeded = bool(mapped_count.get(project.id, 0))
@api.model
def _search_is_milestone_exceeded(self, operator, value):
if not isinstance(value, bool):
raise ValueError(_('Invalid value: %s') % value)
if operator not in ['=', '!=']:
raise ValueError(_('Invalid operator: %s') % operator)
query = """
SELECT P.id
FROM project_project P
LEFT JOIN project_milestone M ON P.id = M.project_id
WHERE M.is_reached IS false
AND M.deadline < CAST(now() AS date)
"""
if (operator == '=' and value is True) or (operator == '!=' and value is False):
operator_new = 'inselect'
else:
operator_new = 'not inselect'
return [('id', operator_new, (query, ()))]
@api.depends('alias_name', 'alias_domain')
def _compute_alias_value(self):
for project in self:
@@ -876,7 +908,7 @@ class Task(models.Model):
return stages.search(['|', ('id', 'in', stages.ids), ('user_id', '=', self.env.user.id)])
active = fields.Boolean(default=True)
name = fields.Char(string='Title', tracking=True, required=True, index=True)
name = fields.Char(string='Title', tracking=True, required=True, index=True, translate=True)
description = fields.Html(string='Description')
priority = fields.Selection([
('0', 'Normal'),
@@ -41,3 +41,4 @@ access_project_collaborator_user,project.collaborator.user,model_project_collabo
access_project_collaborator_portal,project.collaborator.portal,model_project_collaborator,base.group_portal,1,0,0,0
access_project_share_manager,project.share.wizard.manager,model_project_share_wizard,project.group_project_manager,1,1,1,0
access_project_personal_stage,project.personal.stage.user,model_project_task_stage_personal,base.group_user,1,1,1,1
access_project_report_wizard_manager,project.report.wizard.project.manager,model_project_report_wizard,project.group_project_manager,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
41 access_project_collaborator_portal project.collaborator.portal model_project_collaborator base.group_portal 1 0 0 0
42 access_project_share_manager project.share.wizard.manager model_project_share_wizard project.group_project_manager 1 1 1 0
43 access_project_personal_stage project.personal.stage.user model_project_task_stage_personal base.group_user 1 1 1 1
44 access_project_report_wizard_manager project.report.wizard.project.manager model_project_report_wizard project.group_project_manager 1 1 1 0
@@ -8,7 +8,6 @@
* max-width: map-get($container-max-widths, md) - (2 * $o-horizontal-padding);
*/
max-width: 688px;
margin-right: auto;
}
.o_form_project_tasks .note-editable {
+76 -17
View File
@@ -95,6 +95,23 @@
</field>
</record>
<record id="project_task_action_sub_task" model="ir.actions.act_window">
<field name="name">Sub-tasks</field>
<field name="res_model">project.task</field>
<field name="view_mode">tree,kanban,form,calendar,pivot,graph,activity</field>
<field name="search_view_id" ref="project.view_task_search_form"/>
<field name="domain">[('id', 'child_of', active_id), ('id', '!=', active_id)]</field>
<field name="context">{'show_project_update': False, 'default_parent_id': active_id}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No tasks found. Let's create one!
</p><p>
To get things done, use activities and status on tasks.<br/>
Chat in real time or by email to collaborate efficiently.
</p>
</field>
</record>
<!-- Task types -->
<record id="task_type_search" model="ir.ui.view">
<field name="name">project.task.type.search</field>
@@ -276,6 +293,14 @@
<sheet string="Project">
<div class="oe_button_box" name="button_box" groups="base.group_user">
<field name="currency_id" invisible="1"/>
<button class="oe_stat_button" type="object" name="action_view_analytic_account_entries" icon="fa-usd" attrs="{'invisible': [('analytic_account_id', '=', False)]}" groups="analytic.group_analytic_accounting">
<div class="o_form_field o_stat_info">
<span class="o_stat_value">
<field name="analytic_account_balance" widget='monetary'/>
</span>
<span class="o_stat_text">Gross Margin</span>
</div>
</button>
<button class="oe_stat_button" type="action"
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'}"/>
@@ -495,12 +520,15 @@
<field name="analytic_account_id"/>
<field name="stage_id" groups="project.group_project_stages"/>
<field name="tag_ids"/>
<filter string="My Favorites" name="my_projects" domain="[('favorite_user_ids', 'in', uid)]"/>
<separator/>
<filter string="Followed" name="followed_by_me" domain="[('message_is_follower', '=', True)]"/>
<filter string="My Projects" name="own_projects" domain="[('user_id', '=', uid)]"/>
<filter string="My Favorites" name="my_projects" domain="[('favorite_user_ids', 'in', uid)]"/>
<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="Open" name="open" domain="[('stage_id.fold', '=', False)]"/>
<filter string="Late Milestones" name="late_milestones" domain="[('is_milestone_exceeded', '=', True)]"/>
<separator/>
<filter string="Start Date" name="start_date" date="date_start"/>
<filter string="End Date" name="end_date" date="date"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
<group expand="0" string="Group By">
@@ -687,19 +715,23 @@
<div role="menuitem">
<a name="action_view_tasks_analysis" type="object">Tasks Analysis</a>
</div>
<div role="menuitem">
<div role="menuitem" name="project_burndown_menu">
<a name="%(action_project_task_burndown_chart_report)d" type="action">Burndown Chart</a>
</div>
</div>
</div>
<div class="o_kanban_card_manage_settings row">
<div role="menuitem" aria-haspopup="true" class="col-8" groups="project.group_project_manager">
<div role="menuitem" aria-haspopup="true" class="col-6" groups="project.group_project_manager">
<ul class="oe_kanban_colorpicker" data-field="color" role="popup"/>
</div>
<div role="menuitem" class="col-4">
<a t-if="record.privacy_visibility.raw_value == 'portal'" class="dropdown-item" role="menuitem" name="%(project.project_share_wizard_action)d" type="action" groups="project.group_project_manager">Share</a>
<a class="dropdown-item" role="menuitem" type="edit" groups="project.group_project_manager">Edit</a>
<a class="dropdown-item" role="menuitem" type="open" groups="!project.group_project_manager">View</a>
<div role="menuitem" class="col-6" groups="project.group_project_manager">
<a t-if="record.privacy_visibility.raw_value == 'portal'" class="dropdown-item" role="menuitem" name="%(project.project_share_wizard_action)d" type="action">Share</a>
<a class="dropdown-item" role="menuitem" type="edit">Edit</a>
</div>
<div class="o_kanban_card_manage_section o_kanban_manage_view col-12 row pl-0" groups="!project.group_project_manager">
<div role="menuitem" class="w-100">
<a class="dropdown-item mx-0" role="menuitem" type="open">View</a>
</div>
</div>
</div>
</div>
@@ -854,6 +886,15 @@
<span class="o_stat_text">in Recurrence</span>
</div>
</button>
<button name="%(project_task_action_sub_task)d" type="action" class="oe_stat_button" icon="fa-tasks"
attrs="{'invisible' : ['|', '|', ('allow_subtasks', '=', False), ('id', '=', False), ('subtask_count', '=', 0)]}" context="{'default_user_ids': user_ids}">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
<field name="subtask_count" widget="statinfo" nolabel="1"/>
</span>
<span class="o_stat_text">Sub-tasks</span>
</div>
</button>
<button name="action_dependent_tasks" type="object" attrs="{'invisible': [('dependent_tasks_count', '=', 0)]}" class="oe_stat_button" icon="fa-tasks" groups="project.group_project_task_dependencies">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Blocking</span>
@@ -909,6 +950,9 @@
<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">
<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="sequence" widget="handle"/>
@@ -920,14 +964,14 @@
<field name="activity_ids" widget="list_activity" optional="hide"/>
<field name="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}" optional="show"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="hide"/>
<field name="kanban_state" widget="state_selection" optional="hide"/>
<field name="kanban_state" required="0" widget="state_selection" optional="show" options="{'hide_label': True}" nolabel="1" class="d-flex justify-content-center"/>
<field name="stage_id" optional="show"/>
<button name="action_open_task" type="object" title="View Task" string="View Task" class="btn btn-link pull-right"/>
</tree>
</field>
</page>
<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">
<field name="depend_on_ids" nolabel="1" context="{'default_project_id' : project_id}">
<tree editable="bottom">
<field name="is_closed" invisible="1" />
<field name="name" />
@@ -937,7 +981,7 @@
<field name="activity_ids" widget="list_activity" optional="hide"/>
<field name="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}" optional="show" />
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="hide"/>
<field name="kanban_state" widget="state_selection" optional="hide" />
<field name="kanban_state" widget="state_selection" optional="show" options="{'hide_label': True}" nolabel="1"/>
<field name="stage_id" optional="show" />
<button class="oe_link float-right" string="View Task" name="action_open_task" type="object"/>
</tree>
@@ -998,11 +1042,11 @@
<field name="date_last_stage_update" groups="base.group_no_one"/>
</group>
<group string="Working Time to Assign" attrs="{'invisible': [('working_hours_open', '=', 0.0)]}">
<field name="working_hours_open" string="Hours"/>
<field name="working_hours_open" widget="float_time" string="Hours"/>
<field name="working_days_open" string="Days"/>
</group>
<group string="Working Time to Close" attrs="{'invisible': [('working_hours_close', '=', 0.0)]}">
<field name="working_hours_close" string="Hours"/>
<field name="working_hours_close" widget="float_time" string="Hours"/>
<field name="working_days_close" string="Days"/>
</group>
</group>
@@ -1161,6 +1205,9 @@
<field eval="2" name="priority"/>
<field name="arch" type="xml">
<tree string="Tasks" multi_edit="1" sample="1" js_class="project_list">
<field name="legend_normal" invisible="1"/>
<field name="legend_done" invisible="1"/>
<field name="legend_blocked" invisible="1"/>
<field name="message_needaction" invisible="1" readonly="1"/>
<field name="is_closed" invisible="1" />
<field name="allow_subtasks" invisible="1" />
@@ -1176,7 +1223,7 @@
<field name="activity_ids" widget="list_activity" optional="show"/>
<field name="date_deadline" optional="hide" widget="remaining_days" attrs="{'invisible': [('is_closed', '=', True)]}"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="show"/>
<field name="kanban_state" widget="selection" optional="hide" />
<field name="kanban_state" widget="state_selection" optional="show" options="{'hide_label': True}" nolabel="1"/>
<field name="stage_id" invisible="context.get('set_visible',False)" optional="show" readonly="not context.get('default_project_id')"/>
<field name="recurrence_id" invisible="1" />
</tree>
@@ -1399,7 +1446,7 @@
<field name="name">Tags</field>
<field name="model">project.tags</field>
<field name="arch" type="xml">
<tree string="Tags" editable="top" sample="1">
<tree string="Tags" editable="top" sample="1" multi_edit="1">
<field name="name"/>
<field name="color" widget="color_picker" optional="show"/>
</tree>
@@ -1431,12 +1478,24 @@
parent="menu_project_report"
sequence="10"/>
<menuitem id="project_burndown_chart_report_menu_action"
name="Burndown Chart"
action="project.project_burndown_chart_report_action"
parent="menu_project_report"
sequence="11"/>
<menuitem id="rating_rating_menu_project"
action="rating_rating_action_project_report"
parent="menu_project_report"
groups="project.group_project_rating"
sequence="51"/>
<menuitem id="project_update_menu_action"
name="Project Updates"
action="project.project_update_all_report_action"
parent="menu_project_report"
sequence="60"/>
<record id="project_view_kanban_inherit_project" model="ir.ui.view">
<field name="name">project.kanban.inherit.project</field>
<field name="model">project.project</field>
+1
View File
@@ -4,3 +4,4 @@
from . import project_delete_wizard
from . import project_task_type_delete
from . import project_share_wizard
from . import project_report_wizard
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ProjectReportWizard(models.TransientModel):
_name = 'project.report.wizard'
_description = 'Project Report Wizard'
project_id = fields.Many2one('project.project', required=True)
report_type = fields.Selection([
('burndown', 'Burndown Chart'), ('project_update', 'Project Update')
], string='Report Type', required=True, default='burndown')
def action_burndown_report_view(self):
action = self.env['ir.actions.act_window']._for_xml_id('project.action_project_task_burndown_chart_report')
action['context'] = {'search_default_project_id': self.project_id.id}
return action
def action_project_update_report_view(self):
action = self.env['ir.actions.act_window']._for_xml_id('project.project_update_all_action')
action['context'] = {'active_id': self.project_id.id, 'search_default_project_id': self.project_id.id}
return action
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="project_report_wizard_form" model="ir.ui.view">
<field name="name">project.report.wizard.form</field>
<field name="model">project.report.wizard</field>
<field name="arch" type="xml">
<form string="Project">
<group>
<group>
<field name="report_type" invisible="1"/>
<field name="project_id"/>
</group>
</group>
<footer>
<button string="Go to Project Updates" name="action_project_update_report_view" type="object" class="btn-primary" attrs="{'invisible': [('report_type', '!=', 'project_update')]}"/>
<button string="Go to Burndown Chart" name="action_burndown_report_view" type="object" class="btn-primary" attrs="{'invisible': [('report_type', '!=', 'burndown')]}"/>
<button string="Discard" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="project_update_all_report_action" model="ir.actions.act_window">
<field name="name">Select a Project</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">project.report.wizard</field>
<field name="view_mode">form</field>
<field name="context">{'default_report_type': 'project_update'}</field>
<field name="view_id" ref="project_report_wizard_form"/>
<field name="target">new</field>
</record>
<record id="project_burndown_chart_report_action" model="ir.actions.act_window">
<field name="name">Select a Project</field>
<field name="res_model">project.report.wizard</field>
<field name="view_mode">form</field>
<field name="view_id" ref="project_report_wizard_form"/>
<field name="context">{'default_report_type': 'burndown'}</field>
<field name="target">new</field>
</record>
</odoo>
+3 -1
View File
@@ -192,7 +192,9 @@ class SaleOrderLine(models.Model):
"""Generate project values"""
account = self.order_id.analytic_account_id
if not account:
self.order_id._create_analytic_account(prefix=self.product_id.default_code or None)
service_products = self.order_id.order_line.product_id.filtered(lambda p: p.type == 'service' and p.default_code)
default_code = service_products.default_code if len(service_products) == 1 else None
self.order_id._create_analytic_account(prefix=default_code)
account = self.order_id.analytic_account_id
# create the project or duplicate one