[IMP] project, sale_{project, timesheet}: improve user experience
This commit introduces some user experience improvements:
- Always display sale_line_id field whether partner_id is set or not.
- Don't restrict sale orders to the partner_id's one
- Set partner_id to the one of the SOL if partner_id is False
- Remove create option for fields 'user_ids'
- Display the kanban state in the kanban view even in fsm
Task 2585357
closes odoo/odoo#73531
Related: odoo/upgrade#3211
Related: odoo/enterprise#19812
Signed-off-by: Laurent Stukkens (ltu) <ltu@odoo.com>
Co-authored-by: Priyanka Kakadiya <pka@odoo.com>
This commit is contained in:
committed by
Laurent Stukkens (LTU)
co-authored by
Priyanka Kakadiya
parent
2d0133b52b
commit
41116c81cd
@@ -11,20 +11,26 @@ class ReportProjectTaskUser(models.Model):
|
||||
hours_effective = fields.Float('Effective Hours', readonly=True)
|
||||
remaining_hours = fields.Float('Remaining Hours', readonly=True)
|
||||
progress = fields.Float('Progress', group_operator='avg', readonly=True)
|
||||
overtime = fields.Float(readonly=True)
|
||||
|
||||
def _select(self):
|
||||
return super(ReportProjectTaskUser, self)._select() + """,
|
||||
(t.effective_hours * 100) / NULLIF(planned_hours, 0) as progress,
|
||||
t.effective_hours as hours_effective,
|
||||
t.planned_hours - t.effective_hours - t.subtask_effective_hours as remaining_hours,
|
||||
NULLIF(planned_hours, 0) as hours_planned"""
|
||||
select_to_append = """,
|
||||
(t.effective_hours * 100) / NULLIF(t.planned_hours, 0) as progress,
|
||||
t.effective_hours as hours_effective,
|
||||
t.planned_hours - t.effective_hours - t.subtask_effective_hours as remaining_hours,
|
||||
NULLIF(t.planned_hours, 0) as hours_planned,
|
||||
t.overtime as overtime
|
||||
"""
|
||||
return super(ReportProjectTaskUser, self)._select() + select_to_append
|
||||
|
||||
def _group_by(self):
|
||||
return super(ReportProjectTaskUser, self)._group_by() + """,
|
||||
remaining_hours,
|
||||
t.effective_hours,
|
||||
planned_hours
|
||||
"""
|
||||
group_by_append = """,
|
||||
t.effective_hours,
|
||||
t.subtask_effective_hours,
|
||||
t.planned_hours,
|
||||
t.overtime
|
||||
"""
|
||||
return super(ReportProjectTaskUser, self)._group_by() + group_by_append
|
||||
|
||||
@api.model
|
||||
def _fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
|
||||
|
||||
@@ -36,13 +36,13 @@
|
||||
<field name="inherit_id" ref="project.view_task_project_user_search" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//filter[@name='late']" position="after">
|
||||
<filter string="Tasks in Overtime" name="overtime" domain="[('task_id.overtime', '>', 0)]"/>
|
||||
<filter string="Tasks in Overtime" name="overtime" domain="[('overtime', '>', 0)]"/>
|
||||
</xpath>
|
||||
<xpath expr="//filter[@name='my_tasks']" position="before">
|
||||
<filter string="My Team's Projects" name="my_team_projects" domain="[('project_id.user_id.employee_id.parent_id.user_id', '=', uid), ('project_id.user_id', '!=', uid), ('user_ids', '!=', uid), ('user_ids', '!=', False)]"/>
|
||||
</xpath>
|
||||
<xpath expr="//filter[@name='my_tasks']" position="after">
|
||||
<filter string="My Team's Tasks" name="my_team_tasks" domain="[('task_id.user_ids.employee_id.parent_id.user_id', '=', uid)]" />
|
||||
<filter string="My Team's Tasks" name="my_team_tasks" domain="[('user_ids.employee_id.parent_id.user_id', '=', uid)]" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -333,7 +333,7 @@ class Project(models.Model):
|
||||
@api.depends('partner_id.email')
|
||||
def _compute_partner_email(self):
|
||||
for project in self:
|
||||
if project.partner_id and project.partner_id.email != project.partner_email:
|
||||
if project.partner_id.email != project.partner_email:
|
||||
project.partner_email = project.partner_id.email
|
||||
|
||||
def _inverse_partner_email(self):
|
||||
@@ -344,7 +344,7 @@ class Project(models.Model):
|
||||
@api.depends('partner_id.phone')
|
||||
def _compute_partner_phone(self):
|
||||
for project in self:
|
||||
if project.partner_id and project.partner_phone != project.partner_id.phone:
|
||||
if project.partner_phone != project.partner_id.phone:
|
||||
project.partner_phone = project.partner_id.phone
|
||||
|
||||
def _inverse_partner_phone(self):
|
||||
@@ -1325,7 +1325,7 @@ class Task(models.Model):
|
||||
@api.depends('partner_id.email')
|
||||
def _compute_partner_email(self):
|
||||
for task in self:
|
||||
if task.partner_id and task.partner_id.email != task.partner_email:
|
||||
if task.partner_id.email != task.partner_email:
|
||||
task.partner_email = task.partner_id.email
|
||||
|
||||
def _inverse_partner_email(self):
|
||||
@@ -1336,7 +1336,7 @@ class Task(models.Model):
|
||||
@api.depends('partner_id.phone')
|
||||
def _compute_partner_phone(self):
|
||||
for task in self:
|
||||
if task.partner_id and task.partner_phone != task.partner_id.phone:
|
||||
if task.partner_phone != task.partner_id.phone:
|
||||
task.partner_phone = task.partner_id.phone
|
||||
|
||||
def _inverse_partner_phone(self):
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
from odoo import fields, models, tools
|
||||
|
||||
from odoo.addons.rating.models.rating import RATING_LIMIT_MIN
|
||||
|
||||
class ReportProjectTaskUser(models.Model):
|
||||
_name = "report.project.task.user"
|
||||
@@ -30,6 +31,7 @@ class ReportProjectTaskUser(models.Model):
|
||||
working_hours_open = fields.Float(string='Working Hours to Assign', digits=(16, 2), readonly=True, group_operator="avg", help="Number of Working Hours to open the task")
|
||||
working_hours_close = fields.Float(string='Working Hours to Close', digits=(16, 2), readonly=True, group_operator="avg", help="Number of Working Hours to close the task")
|
||||
rating_last_value = fields.Float('Rating Value (/5)', group_operator="avg", readonly=True, groups="project.group_project_rating")
|
||||
rating_avg = fields.Float('Average Rating', readonly=True)
|
||||
priority = fields.Selection([
|
||||
('0', 'Low'),
|
||||
('1', 'Normal'),
|
||||
@@ -44,61 +46,85 @@ class ReportProjectTaskUser(models.Model):
|
||||
partner_id = fields.Many2one('res.partner', string='Customer', readonly=True)
|
||||
stage_id = fields.Many2one('project.task.type', string='Stage', readonly=True)
|
||||
task_id = fields.Many2one('project.task', string='Tasks', readonly=True)
|
||||
active = fields.Boolean(readonly=True)
|
||||
tag_ids = fields.Many2many('project.tags', relation='project_tags_project_task_rel',
|
||||
column1='project_task_id', column2='project_tags_id',
|
||||
string='Tags', readonly=True)
|
||||
parent_id = fields.Many2one('project.task', string='Parent Task', readonly=True)
|
||||
|
||||
def _select(self):
|
||||
select_str = """
|
||||
SELECT
|
||||
(select 1 ) AS nbr,
|
||||
t.id as id,
|
||||
t.id as task_id,
|
||||
t.create_date as create_date,
|
||||
t.date_assign as date_assign,
|
||||
t.date_end as date_end,
|
||||
t.date_last_stage_update as date_last_stage_update,
|
||||
t.date_deadline as date_deadline,
|
||||
t.project_id,
|
||||
t.priority,
|
||||
t.name as name,
|
||||
t.company_id,
|
||||
t.partner_id,
|
||||
t.stage_id as stage_id,
|
||||
t.kanban_state as state,
|
||||
NULLIF(t.rating_last_value, 0) as rating_last_value,
|
||||
t.working_days_close as working_days_close,
|
||||
t.working_days_open as working_days_open,
|
||||
t.working_hours_open as working_hours_open,
|
||||
t.working_hours_close as working_hours_close,
|
||||
(extract('epoch' from (t.date_deadline-(now() at time zone 'UTC'))))/(3600*24) as delay_endings_days
|
||||
return """
|
||||
(select 1) AS nbr,
|
||||
t.id as id,
|
||||
t.id as task_id,
|
||||
t.active,
|
||||
t.create_date as create_date,
|
||||
t.date_assign as date_assign,
|
||||
t.date_end as date_end,
|
||||
t.date_last_stage_update as date_last_stage_update,
|
||||
t.date_deadline as date_deadline,
|
||||
t.project_id,
|
||||
t.priority,
|
||||
t.name as name,
|
||||
t.company_id,
|
||||
t.partner_id,
|
||||
t.parent_id as parent_id,
|
||||
t.stage_id as stage_id,
|
||||
t.kanban_state as state,
|
||||
NULLIF(t.rating_last_value, 0) as rating_last_value,
|
||||
AVG(rt.rating) as rating_avg,
|
||||
t.working_days_close as working_days_close,
|
||||
t.working_days_open as working_days_open,
|
||||
t.working_hours_open as working_hours_open,
|
||||
t.working_hours_close as working_hours_close,
|
||||
(extract('epoch' from (t.date_deadline-(now() at time zone 'UTC'))))/(3600*24) as delay_endings_days
|
||||
"""
|
||||
return select_str
|
||||
|
||||
def _group_by(self):
|
||||
group_by_str = """
|
||||
GROUP BY
|
||||
t.id,
|
||||
t.create_date,
|
||||
t.write_date,
|
||||
t.date_assign,
|
||||
t.date_end,
|
||||
t.date_deadline,
|
||||
t.date_last_stage_update,
|
||||
t.project_id,
|
||||
t.priority,
|
||||
t.name,
|
||||
t.company_id,
|
||||
t.partner_id,
|
||||
t.stage_id
|
||||
return """
|
||||
t.id,
|
||||
t.active,
|
||||
t.create_date,
|
||||
t.date_assign,
|
||||
t.date_end,
|
||||
t.date_last_stage_update,
|
||||
t.date_deadline,
|
||||
t.project_id,
|
||||
t.priority,
|
||||
t.name,
|
||||
t.company_id,
|
||||
t.partner_id,
|
||||
t.parent_id,
|
||||
t.stage_id,
|
||||
t.kanban_state,
|
||||
t.rating_last_value,
|
||||
t.working_days_close,
|
||||
t.working_days_open,
|
||||
t.working_hours_open,
|
||||
t.working_hours_close
|
||||
"""
|
||||
|
||||
def _from(self):
|
||||
return f"""
|
||||
project_task t
|
||||
LEFT JOIN project_task_user_rel tu on t.id=tu.task_id
|
||||
LEFT JOIN rating_rating rt ON rt.parent_res_id = t.id
|
||||
AND rt.res_model = 'project.task'
|
||||
AND rt.consumed = True
|
||||
AND rt.rating >= {RATING_LIMIT_MIN}
|
||||
"""
|
||||
|
||||
def _where(self):
|
||||
return """
|
||||
t.project_id IS NOT NULL
|
||||
"""
|
||||
return group_by_str
|
||||
|
||||
def init(self):
|
||||
tools.drop_view_if_exists(self._cr, self._table)
|
||||
self._cr.execute("""
|
||||
CREATE view %s as
|
||||
%s
|
||||
FROM project_task t
|
||||
LEFT JOIN project_task_user_rel tu on t.id=tu.task_id
|
||||
WHERE t.active = 'true'
|
||||
AND t.project_id IS NOT NULL
|
||||
%s
|
||||
""" % (self._table, self._select(), self._group_by()))
|
||||
CREATE view %s as
|
||||
SELECT %s
|
||||
FROM %s
|
||||
WHERE %s
|
||||
GROUP BY %s
|
||||
""" % (self._table, self._select(), self._from(), self._where(), self._group_by()))
|
||||
|
||||
@@ -45,21 +45,23 @@
|
||||
<field name="arch" type="xml">
|
||||
<search string="Tasks Analysis">
|
||||
<field name="project_id"/>
|
||||
<field name="name" />
|
||||
<field name="name"/>
|
||||
<field name="stage_id"/>
|
||||
<field name="user_ids"/>
|
||||
<field name="partner_id" filter_domain="[('partner_id', 'child_of', self)]"/>
|
||||
<field name="name" string="Tags" filter_domain="[('task_id.tag_ids', 'ilike', self)]"/>
|
||||
<field name="active"/>
|
||||
<field name="parent_id"/>
|
||||
<field name="name" string="Tags" filter_domain="[('tag_ids', 'ilike', self)]"/>
|
||||
<field name="date_assign"/>
|
||||
<field name="date_end"/>
|
||||
<field name="date_deadline"/>
|
||||
<field name="date_last_stage_update"/>
|
||||
<filter string="My Projects" name="own_projects" domain="[('project_id.user_id', '=', uid)]"/>
|
||||
<filter string="My Tasks" name="my_tasks" domain="[('task_id.user_ids', 'in', uid)]"/>
|
||||
<filter string="My Tasks" name="my_tasks" domain="[('user_ids', 'in', uid)]"/>
|
||||
<separator/>
|
||||
<filter string="Starred" name="starred" domain="[('task_id.priority', 'in', [0, 1])]"/>
|
||||
<filter string="Starred" name="starred" domain="[('priority', 'in', [0, 1])]"/>
|
||||
<separator/>
|
||||
<filter string="Tasks Late" name="late" domain="[('task_id.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter string="Tasks Late" name="late" domain="[('date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<separator/>
|
||||
<filter string="Unassigned Tasks" name="unassigned" domain="[('user_ids', '=', False)]"/>
|
||||
<separator/>
|
||||
|
||||
@@ -500,11 +500,11 @@
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Project">
|
||||
<field name="name" string="Project"/>
|
||||
<field name="tag_ids"/>
|
||||
<field name="user_id" string="Project Manager"/>
|
||||
<field name="partner_id" string="Customer" filter_domain="[('partner_id', 'child_of', self)]"/>
|
||||
<field name="analytic_account_id"/>
|
||||
<field name="stage_id" groups="project.group_project_stages"/>
|
||||
<field name="tag_ids"/>
|
||||
<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)]"/>
|
||||
@@ -914,7 +914,7 @@
|
||||
<field name="display_project_id" string="Project" attrs="{'invisible': [('parent_id', '=', False)]}" domain="[('active', '=', True), ('company_id', '=', company_id)]"/>
|
||||
<field name="user_ids"
|
||||
class="o_task_user_field"
|
||||
options="{'no_open': True}"
|
||||
options="{'no_open': True, 'no_quick_create': True}"
|
||||
widget="many2many_avatar_user"
|
||||
domain="[('share', '=', False), ('active', '=', True)]"/>
|
||||
</group>
|
||||
@@ -1173,7 +1173,7 @@
|
||||
</b>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right" t-if="!selection_mode">
|
||||
<field name="kanban_state" widget="state_selection" groups="base.group_user" invisible="context.get('fsm_mode', False)"/>
|
||||
<field name="kanban_state" widget="state_selection" groups="base.group_user"/>
|
||||
<t t-if="record.user_ids.raw_value"><field name="user_ids" widget="many2many_avatar_user"/></t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,13 @@ LOCKED_FIELD_STATES = {
|
||||
for state in {'done', 'cancel'}
|
||||
}
|
||||
|
||||
INVOICE_STATUS = [
|
||||
('upselling', 'Upselling Opportunity'),
|
||||
('invoiced', 'Fully Invoiced'),
|
||||
('to invoice', 'To Invoice'),
|
||||
('no', 'Nothing to Invoice')
|
||||
]
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_name = "sale.order"
|
||||
@@ -218,12 +225,7 @@ class SaleOrder(models.Model):
|
||||
|
||||
invoice_count = fields.Integer(string='Invoice Count', compute='_get_invoiced')
|
||||
invoice_ids = fields.Many2many("account.move", string='Invoices', compute="_get_invoiced", copy=False, search="_search_invoice_ids")
|
||||
invoice_status = fields.Selection([
|
||||
('upselling', 'Upselling Opportunity'),
|
||||
('invoiced', 'Fully Invoiced'),
|
||||
('to invoice', 'To Invoice'),
|
||||
('no', 'Nothing to Invoice')
|
||||
], string='Invoice Status', compute='_get_invoice_status', store=True)
|
||||
invoice_status = fields.Selection(INVOICE_STATUS, string='Invoice Status', compute='_get_invoice_status', store=True)
|
||||
|
||||
note = fields.Html(
|
||||
'Terms and conditions',
|
||||
|
||||
@@ -150,7 +150,7 @@ class ProjectTask(models.Model):
|
||||
'sale.order.line', 'Sales Order Item',
|
||||
copy=False, tracking=True, index='btree_not_null', recursive=True,
|
||||
compute='_compute_sale_line', store=True, readonly=False,
|
||||
domain="[('company_id', '=', company_id), ('is_service', '=', True), ('order_partner_id', 'child_of', commercial_partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done'])]",
|
||||
domain="[('company_id', '=', company_id), ('is_service', '=', True), ('order_partner_id', '=?', partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done'])]",
|
||||
help="Sales Order Item to which the time spent on this task will be added, in order to be invoiced to your customer.")
|
||||
project_sale_order_id = fields.Many2one('sale.order', string="Project's sale order", related='project_id.sale_order_id')
|
||||
task_to_invoice = fields.Boolean("To invoice", compute='_compute_task_to_invoice', search='_search_task_to_invoice', groups='sales_team.group_sale_salesman_all_leads')
|
||||
@@ -271,6 +271,10 @@ class ProjectTask(models.Model):
|
||||
operator_new = 'not inselect'
|
||||
return [('sale_order_id', operator_new, (query, ()))]
|
||||
|
||||
@api.onchange('sale_line_id')
|
||||
def _onchange_partner_id(self):
|
||||
if not self.partner_id and self.sale_line_id:
|
||||
self.partner_id = self.sale_line_id.order_partner_id
|
||||
|
||||
class ProjectTaskRecurrence(models.Model):
|
||||
_inherit = 'project.task.recurrence'
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='partner_phone']" position="after">
|
||||
<field name="sale_order_id" attrs="{'invisible': True}" groups="sales_team.group_sale_salesman"/>
|
||||
<field name="sale_line_id" string="Sales Order Item" attrs="{'invisible': [('partner_id', '=', False)]}" options='{"no_open": True}' readonly="1" context="{'create': False, 'edit': False, 'delete': False}"/>
|
||||
<field name="sale_line_id" string="Sales Order Item" options='{"no_open": True}' readonly="1" context="{'create': False, 'edit': False, 'delete': False}"/>
|
||||
<field name="commercial_partner_id" invisible="1" />
|
||||
</xpath>
|
||||
</field>
|
||||
|
||||
@@ -437,7 +437,7 @@ class ProjectTask(models.Model):
|
||||
has_multi_sol = fields.Boolean(compute='_compute_has_multi_sol', compute_sudo=True)
|
||||
allow_billable = fields.Boolean(related="project_id.allow_billable")
|
||||
timesheet_product_id = fields.Many2one(related="project_id.timesheet_product_id")
|
||||
remaining_hours_so = fields.Float('Remaining Hours on SO', compute='_compute_remaining_hours_so', compute_sudo=True)
|
||||
remaining_hours_so = fields.Float('Remaining Hours on SO', compute='_compute_remaining_hours_so', search='_search_remaining_hours_so', compute_sudo=True)
|
||||
remaining_hours_available = fields.Boolean(related="sale_line_id.remaining_hours_available")
|
||||
|
||||
@property
|
||||
@@ -468,6 +468,10 @@ class ProjectTask(models.Model):
|
||||
for task in self:
|
||||
task.remaining_hours_so = mapped_remaining_hours[task._origin.id]
|
||||
|
||||
@api.model
|
||||
def _search_remaining_hours_so(self, operator, value):
|
||||
return [('sale_line_id.remaining_hours', operator, value)]
|
||||
|
||||
@api.depends('so_analytic_account_id.active')
|
||||
def _compute_analytic_account_active(self):
|
||||
super()._compute_analytic_account_active()
|
||||
|
||||
@@ -212,7 +212,7 @@
|
||||
<xpath expr="//field[@name='sale_line_id']" position="attributes">
|
||||
<attribute name="context">{'with_remaining_hours': True, 'with_price_unit': True}</attribute>
|
||||
<attribute name="attrs">
|
||||
{'invisible': ['|', ('allow_billable', '=', False), ('partner_id', '=', False)]}
|
||||
{'invisible': [('allow_billable', '=', False)]}
|
||||
</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='sale_order_id']" position="attributes">
|
||||
|
||||
Reference in New Issue
Block a user