diff --git a/addons/hr_timesheet/report/project_report.py b/addons/hr_timesheet/report/project_report.py index 2f5890f834c..be4238b46db 100644 --- a/addons/hr_timesheet/report/project_report.py +++ b/addons/hr_timesheet/report/project_report.py @@ -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): diff --git a/addons/hr_timesheet/report/project_report_view.xml b/addons/hr_timesheet/report/project_report_view.xml index fe3fdaf6a76..710b5eafd41 100644 --- a/addons/hr_timesheet/report/project_report_view.xml +++ b/addons/hr_timesheet/report/project_report_view.xml @@ -36,13 +36,13 @@ - + - + diff --git a/addons/project/models/project.py b/addons/project/models/project.py index fd26fbea16b..b9f88d54c59 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -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): diff --git a/addons/project/report/project_report.py b/addons/project/report/project_report.py index 1c7c3aec975..a40c6dfee97 100644 --- a/addons/project/report/project_report.py +++ b/addons/project/report/project_report.py @@ -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())) diff --git a/addons/project/report/project_report_views.xml b/addons/project/report/project_report_views.xml index d968e6086a3..19f8c10827c 100644 --- a/addons/project/report/project_report_views.xml +++ b/addons/project/report/project_report_views.xml @@ -45,21 +45,23 @@ - + - + + + - + - + - + diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index 8dc9e751a2d..2b2b6c4d78b 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -500,11 +500,11 @@ + - @@ -914,7 +914,7 @@ @@ -1173,7 +1173,7 @@
- +
diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index c8ca70703c8..917287510a4 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -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', diff --git a/addons/sale_project/models/project.py b/addons/sale_project/models/project.py index aaf71760940..7bbd1ca69d8 100644 --- a/addons/sale_project/models/project.py +++ b/addons/sale_project/models/project.py @@ -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' diff --git a/addons/sale_project/views/project_task_views.xml b/addons/sale_project/views/project_task_views.xml index f899fe78f5a..d1e80f5bbd8 100644 --- a/addons/sale_project/views/project_task_views.xml +++ b/addons/sale_project/views/project_task_views.xml @@ -67,7 +67,7 @@ - +
diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index 10521746604..ae4b471f359 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -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() diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index b9f29741aa0..ecaedc6bc3b 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -212,7 +212,7 @@ {'with_remaining_hours': True, 'with_price_unit': True} - {'invisible': ['|', ('allow_billable', '=', False), ('partner_id', '=', False)]} + {'invisible': [('allow_billable', '=', False)]}