diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py index 19dcf45fa7e..c7de0f8482e 100644 --- a/addons/hr_timesheet/models/project.py +++ b/addons/hr_timesheet/models/project.py @@ -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): diff --git a/addons/hr_timesheet/views/project_views.xml b/addons/hr_timesheet/views/project_views.xml index 04c0a9538d1..ab2aa90fba3 100644 --- a/addons/hr_timesheet/views/project_views.xml +++ b/addons/hr_timesheet/views/project_views.xml @@ -212,7 +212,7 @@ - + Hours Spent on Sub-tasks: Days Spent on Sub-tasks: @@ -340,6 +340,20 @@ + + project.project.view.inherit.timesheet + project.project + + + + + + + + + + + project.task.view.graph.inherited project.task diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index 0c26d807ba3..fc30d6266d4 100644 --- a/addons/project/__manifest__.py +++ b/addons/project/__manifest__.py @@ -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', diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 64d3e1d738d..858b4fbc1e2 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -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'), diff --git a/addons/project/security/ir.model.access.csv b/addons/project/security/ir.model.access.csv index 99ba6bdd054..9e76ea5002f 100644 --- a/addons/project/security/ir.model.access.csv +++ b/addons/project/security/ir.model.access.csv @@ -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 diff --git a/addons/project/static/src/css/project.css b/addons/project/static/src/css/project.css index 5ec2db81ed7..5366b7d8e17 100644 --- a/addons/project/static/src/css/project.css +++ b/addons/project/static/src/css/project.css @@ -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 { diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index 8418c09f8ac..35f0b1b48f6 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -95,6 +95,23 @@ + + Sub-tasks + project.task + tree,kanban,form,calendar,pivot,graph,activity + + [('id', 'child_of', active_id), ('id', '!=', active_id)] + {'show_project_update': False, 'default_parent_id': active_id} + + + No tasks found. Let's create one! + + To get things done, use activities and status on tasks. + Chat in real time or by email to collaborate efficiently. + + + + project.task.type.search @@ -276,6 +293,14 @@ + + + + + + Gross Margin + + @@ -495,12 +520,15 @@ - - - + + + - + + + + @@ -687,19 +715,23 @@ Tasks Analysis - + Burndown Chart - + - - Share - Edit - View + + Share + Edit + + + + View + @@ -854,6 +886,15 @@ in Recurrence + + + + + + Sub-tasks + + Blocking @@ -909,6 +950,9 @@ + + + @@ -920,14 +964,14 @@ - + - + @@ -937,7 +981,7 @@ - + @@ -998,11 +1042,11 @@ - + - + @@ -1161,6 +1205,9 @@ + + + @@ -1176,7 +1223,7 @@ - + @@ -1399,7 +1446,7 @@ Tags project.tags - + @@ -1431,12 +1478,24 @@ 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 96e583bead0..e3cb21067c6 100644 --- a/addons/project/wizard/__init__.py +++ b/addons/project/wizard/__init__.py @@ -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 diff --git a/addons/project/wizard/project_report_wizard.py b/addons/project/wizard/project_report_wizard.py new file mode 100644 index 00000000000..f550a62e87b --- /dev/null +++ b/addons/project/wizard/project_report_wizard.py @@ -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 diff --git a/addons/project/wizard/project_report_wizard_views.xml b/addons/project/wizard/project_report_wizard_views.xml new file mode 100644 index 00000000000..d3584c930e9 --- /dev/null +++ b/addons/project/wizard/project_report_wizard_views.xml @@ -0,0 +1,40 @@ + + + + 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/sale_project/models/sale_order.py b/addons/sale_project/models/sale_order.py index acae6135277..dc641d5087f 100644 --- a/addons/sale_project/models/sale_order.py +++ b/addons/sale_project/models/sale_order.py @@ -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
+ No tasks found. Let's create one! +
+ To get things done, use activities and status on tasks. + Chat in real time or by email to collaborate efficiently. +