diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py index 3c1e14451e5..a632d24928b 100644 --- a/addons/hr_timesheet/models/project.py +++ b/addons/hr_timesheet/models/project.py @@ -5,7 +5,7 @@ from collections import defaultdict from odoo import models, fields, api, _ from odoo.exceptions import UserError, ValidationError, RedirectWarning - +from odoo.addons.rating.models.rating_data import OPERATOR_MAPPING PROJECT_TASK_READABLE_FIELDS = { 'allow_subtasks', @@ -252,6 +252,7 @@ class Task(models.Model): analytic_account_active = fields.Boolean("Active Analytic Account", compute='_compute_analytic_account_active', compute_sudo=True) allow_timesheets = fields.Boolean("Allow timesheets", related='project_id.allow_timesheets', help="Timesheets can be logged on this task.", readonly=True) remaining_hours = fields.Float("Remaining Hours", compute='_compute_remaining_hours', store=True, readonly=True, help="Total remaining time, can be re-estimated periodically by the assignee of the task.") + remaining_hours_percentage = fields.Float(compute='_compute_remaining_hours_percentage', search='_search_remaining_hours_percentage') effective_hours = fields.Float("Hours Spent", compute='_compute_effective_hours', compute_sudo=True, store=True, help="Time spent on this task, excluding its sub-tasks.") total_hours_spent = fields.Float("Total Hours", compute='_compute_total_hours_spent', store=True, help="Time spent on this task, including its sub-tasks.") progress = fields.Float("Progress", compute='_compute_progress_hours', store=True, group_operator="avg", help="Display progress of current task.") @@ -301,6 +302,26 @@ class Task(models.Model): task.progress = 0.0 task.overtime = 0 + @api.depends('planned_hours', 'remaining_hours') + def _compute_remaining_hours_percentage(self): + for task in self: + if task.planned_hours > 0.0: + task.remaining_hours_percentage = task.remaining_hours / task.planned_hours + else: + task.remaining_hours_percentage = 0.0 + + def _search_remaining_hours_percentage(self, operator, value): + if operator not in OPERATOR_MAPPING: + raise NotImplementedError('This operator %s is not supported in this search method.' % operator) + query = f""" + SELECT id + FROM {self._table} + WHERE remaining_hours > 0 + AND planned_hours > 0 + AND remaining_hours / planned_hours {operator} %s + """ + return [('id', 'inselect', (query, (value,)))] + @api.depends('effective_hours', 'subtask_effective_hours', 'planned_hours') def _compute_remaining_hours(self): for task in self: diff --git a/addons/hr_timesheet/views/project_views.xml b/addons/hr_timesheet/views/project_views.xml index d45911dfebb..c8debe8153f 100644 --- a/addons/hr_timesheet/views/project_views.xml +++ b/addons/hr_timesheet/views/project_views.xml @@ -111,12 +111,12 @@ - + - + @@ -245,9 +245,9 @@ - + @@ -351,6 +351,7 @@ + diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index a74bd330c4f..7a0a2137bde 100644 --- a/addons/project/__manifest__.py +++ b/addons/project/__manifest__.py @@ -34,7 +34,6 @@ '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/project_milestone_views.xml', 'views/res_partner_views.xml', diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 1410b39ae0a..6b4478af4f3 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -308,16 +308,16 @@ class Project(models.Model): "with Tasks (or optionally Issues if the Issue Tracker module is installed).") alias_value = fields.Char(string='Alias email', compute='_compute_alias_value') privacy_visibility = fields.Selection([ - ('followers', 'Invited employees'), - ('employees', 'All employees'), - ('portal', 'Invited portal users and all employees'), + ('followers', 'Invited internal users'), + ('employees', 'All internal users'), + ('portal', 'Invited portal users and all internal users'), ], string='Visibility', required=True, default='portal', help="Defines the visibility of the tasks of the project:\n" - "- Invited employees: employees may only see the followed project and tasks.\n" - "- All employees: employees may see all project and tasks.\n" - "- Invited portal users and all employees: employees may see everything." + "- Invited internal users: internal users may only see the followed project and tasks.\n" + "- All internal users: internal users may see all project and tasks.\n" + "- Invited portal users and all internal users: internal users may see everything." " Invited portal users may see project and tasks followed by.\n" " them or by someone of their company.") doc_count = fields.Integer(compute='_compute_attached_docs_count', string="Number of documents attached") @@ -531,6 +531,7 @@ class Project(models.Model): project.message_subscribe(partner_ids=follower.partner_id.ids, subtype_ids=follower.subtype_ids.ids) if 'tasks' not in default: self.map_tasks(project.id) + return project @api.model @@ -857,7 +858,7 @@ class Project(models.Model): 'number': f'{round(100 * self.rating_avg_percentage, 2)} %', 'action_type': 'object', 'action': 'action_view_all_rating', - 'show': self.rating_active and self.rating_count > 0, + 'show': self.rating_active, 'sequence': 15, }) if self.user_has_groups('project.group_project_user'): @@ -1124,7 +1125,7 @@ class Task(models.Model): working_days_close = fields.Float(compute='_compute_elapsed', string='Working Days to Close', store=True, group_operator="avg") # 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') + is_private = fields.Boolean(compute='_compute_is_private', search='_search_is_private') allow_milestones = fields.Boolean(related='project_id.allow_milestones') milestone_id = fields.Many2one( 'project.milestone', @@ -1153,6 +1154,7 @@ class Task(models.Model): column2="task_id", string="Block", copy=False, domain="[('allow_task_dependencies', '=', True), ('id', '!=', id)]") dependent_tasks_count = fields.Integer(string="Dependent Tasks", compute='_compute_dependent_tasks_count') + is_blocked = fields.Boolean(compute='_compute_is_blocked', store=True, recursive=True) # Project sharing fields display_parent_task_button = fields.Boolean(compute='_compute_display_parent_task_button', compute_sudo=True) @@ -1277,6 +1279,16 @@ class Task(models.Model): for task in self: task.is_private = not task.project_id and not task.parent_id + def _search_is_private(self, operator, value): + if not isinstance(value, bool): + raise ValueError(_('Value should be True or False (not %s)'), value) + if operator not in ['=', '!=']: + raise NotImplementedError(_('Operation should be = or != (not %s)'), value) + if (operator == '=' and value) or (operator == '!=' and not value): + return [('project_id', '=', False)] + else: + return [('project_id', '!=', False)] + @api.depends('parent_id.ancestor_id') def _compute_ancestor_id(self): for task in self: @@ -1455,6 +1467,11 @@ class Task(models.Model): for task in tasks_with_dependency: task.dependent_tasks_count = dependent_tasks_count_dict.get(task.id, 0) + @api.depends('depend_on_ids.is_closed', 'depend_on_ids.is_blocked') + def _compute_is_blocked(self): + for task in self: + task.is_blocked = any(not blocking_task.is_closed or blocking_task.is_blocked for blocking_task in task.depend_on_ids) + @api.depends('partner_id.email') def _compute_partner_email(self): for task in self: diff --git a/addons/project/security/ir.model.access.csv b/addons/project/security/ir.model.access.csv index 31ad16d969d..9267d73855f 100644 --- a/addons/project/security/ir.model.access.csv +++ b/addons/project/security/ir.model.access.csv @@ -43,6 +43,5 @@ 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 access_project_milestone_reach_wizard_project_user,project.milestone.reach.wizard.project.user,model_project_milestone_reach_wizard,project.group_project_user,1,1,1,0 access_project_milestone_reach_line_wizard_project_user,project.milestone.reach.line.wizard.project.user,model_project_milestone_reach_line_wizard,project.group_project_user,1,1,1,0 diff --git a/addons/project/views/project_sharing_views.xml b/addons/project/views/project_sharing_views.xml index f5d06ca7d48..8a9de75963f 100644 --- a/addons/project/views/project_sharing_views.xml +++ b/addons/project/views/project_sharing_views.xml @@ -123,12 +123,12 @@ - - + + diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index 1fdb6edf4e7..21bd539a854 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -16,7 +16,7 @@ project.task - + @@ -32,13 +32,24 @@ - - + + + + + + + + + + + + + @@ -1139,7 +1150,11 @@ - + + + + + - + - + + @@ -1165,18 +1184,31 @@ - + + + + + + + - + + + + + @@ -1409,18 +1441,15 @@ - - - - - + + @@ -1435,6 +1464,9 @@ decoration-warning="rating_last_text == 'ok'" decoration-success="rating_last_text == 'top'" attrs="{'invisible': ['|', ('rating_active', '=', False), ('rating_last_text', '=', 'none')]}" class="font-weight-bold" widget="badge" optional="hide" groups="project.group_project_rating"/> + + + @@ -1730,24 +1762,12 @@ parent="menu_project_report" sequence="10"/> - - - - project.kanban.inherit.project project.project diff --git a/addons/project/wizard/__init__.py b/addons/project/wizard/__init__.py index e7f558db232..76a9bc260fa 100644 --- a/addons/project/wizard/__init__.py +++ b/addons/project/wizard/__init__.py @@ -5,4 +5,3 @@ from . import project_delete_wizard from . import project_milestone_reach_wizard from . import project_task_type_delete from . import project_share_wizard -from . import project_report_wizard diff --git a/addons/project/wizard/project_report_wizard.py b/addons/project/wizard/project_report_wizard.py deleted file mode 100644 index f550a62e87b..00000000000 --- a/addons/project/wizard/project_report_wizard.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- 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 diff --git a/addons/project/wizard/project_report_wizard_views.xml b/addons/project/wizard/project_report_wizard_views.xml deleted file mode 100644 index d3584c930e9..00000000000 --- a/addons/project/wizard/project_report_wizard_views.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - project.report.wizard.form - project.report.wizard - - - - - - - - - - - - - - - Select a Project - ir.actions.act_window - project.report.wizard - form - {'default_report_type': 'project_update'} - - new - - - Select a Project - project.report.wizard - form - - {'default_report_type': 'burndown'} - new - - diff --git a/addons/project_hr_expense/views/project_project_views.xml b/addons/project_hr_expense/views/project_project_views.xml index f0976721183..cdccddce8e4 100644 --- a/addons/project_hr_expense/views/project_project_views.xml +++ b/addons/project_hr_expense/views/project_project_views.xml @@ -8,7 +8,7 @@ 40 - + diff --git a/addons/project_mrp/views/project_views.xml b/addons/project_mrp/views/project_views.xml index 31c24e9e997..71d479c2725 100644 --- a/addons/project_mrp/views/project_views.xml +++ b/addons/project_mrp/views/project_views.xml @@ -8,7 +8,7 @@ @@ -18,7 +18,7 @@ diff --git a/addons/sale_project/__manifest__.py b/addons/sale_project/__manifest__.py index 9497db3fbf4..3210d881c68 100644 --- a/addons/sale_project/__manifest__.py +++ b/addons/sale_project/__manifest__.py @@ -19,6 +19,7 @@ This module allows to generate a project/task from sales orders. 'views/sale_order_views.xml', 'views/sale_project_portal_templates.xml', 'views/project_sharing_views.xml', + 'views/project_views.xml', ], 'assets': { 'web.assets_backend': [ diff --git a/addons/sale_project/models/project.py b/addons/sale_project/models/project.py index 3b12797eb9e..b3a3a680619 100644 --- a/addons/sale_project/models/project.py +++ b/addons/sale_project/models/project.py @@ -468,7 +468,7 @@ class ProjectTask(models.Model): sale_order_id = fields.Many2one('sale.order', 'Sales Order', compute='_compute_sale_order_id', store=True, help="Sales order to which the task is linked.") sale_line_id = fields.Many2one( 'sale.order.line', 'Sales Order Item', - copy=False, tracking=True, index='btree_not_null', recursive=True, + copy=True, 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', '=?', 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.") diff --git a/addons/sale_project/views/product_views.xml b/addons/sale_project/views/product_views.xml index 36710263ea8..474ee8286ea 100644 --- a/addons/sale_project/views/product_views.xml +++ b/addons/sale_project/views/product_views.xml @@ -13,8 +13,6 @@ - - diff --git a/addons/sale_project/views/project_views.xml b/addons/sale_project/views/project_views.xml new file mode 100644 index 00000000000..fe5ac52ca3f --- /dev/null +++ b/addons/sale_project/views/project_views.xml @@ -0,0 +1,6 @@ + + + + {'default_allow_billable': True} + + diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index 27b1061842a..9b445b83f05 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -41,7 +41,7 @@ - + @@ -108,11 +108,18 @@ sale.service.form.view.inherit project.task - + + + + + + + + diff --git a/odoo/addons/test_main_flows/static/tests/tours/main_flow.js b/odoo/addons/test_main_flows/static/tests/tours/main_flow.js index bb170b1aeef..5622c97b63b 100644 --- a/odoo/addons/test_main_flows/static/tests/tours/main_flow.js +++ b/odoo/addons/test_main_flows/static/tests/tours/main_flow.js @@ -887,7 +887,7 @@ tour.stepUtils.mobileModifier(tour.stepUtils.autoExpandMoreButtons('.o_control_p content: _t('See Project'), position: 'right', }, -tour.stepUtils.mobileModifier(tour.stepUtils.autoExpandMoreButtons('.o_control_panel .breadcrumb:contains("the_flow.project")')), +tour.stepUtils.autoExpandMoreButtons('.o_control_panel .breadcrumb:contains("the_flow.project")'), { trigger: '.oe_button_box .oe_stat_button:has(span:contains("Recorded"))', extra_trigger: '.o_form_readonly',