diff --git a/addons/sale_project/models/project.py b/addons/sale_project/models/project.py index 59aad93a856..523fad6b108 100644 --- a/addons/sale_project/models/project.py +++ b/addons/sale_project/models/project.py @@ -71,15 +71,25 @@ class ProjectTask(models.Model): # Actions # --------------------------------------------------- + def _get_action_view_so_ids(self): + return self.sale_order_id.ids + def action_view_so(self): self.ensure_one() - return { + so_ids = self._get_action_view_so_ids() + action_window = { "type": "ir.actions.act_window", "res_model": "sale.order", - "views": [[False, "form"]], - "res_id": self.sale_order_id.id, + "name": "Sales Order", + "views": [[False, "tree"], [False, "form"]], "context": {"create": False, "show_sale": True}, + "domain": [["id", "in", so_ids]], } + if len(so_ids) == 1: + action_window["views"] = [[False, "form"]] + action_window["res_id"] = so_ids[0] + + return action_window def rating_get_partner_id(self): partner = self.partner_id or self.sale_line_id.order_id.partner_id diff --git a/addons/sale_timesheet/models/account_move.py b/addons/sale_timesheet/models/account_move.py index e98e479576b..91e5ac99ce0 100644 --- a/addons/sale_timesheet/models/account_move.py +++ b/addons/sale_timesheet/models/account_move.py @@ -74,6 +74,6 @@ class AccountMoveLine(models.Model): """ return [ ('so_line', 'in', sale_line_delivery.ids), - ('timesheet_invoice_id', '=', False), - ('project_id', '!=', False) + ('project_id', '!=', False), + '|', ('timesheet_invoice_id', '=', False), ('timesheet_invoice_id.state', '=', 'cancel') ] diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index 876e09efe60..31a06694f12 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -126,6 +126,19 @@ class ProjectTask(models.Model): is_project_map_empty = fields.Boolean("Is Project map empty", compute='_compute_is_project_map_empty') allow_billable = fields.Boolean(related="project_id.allow_billable") + @api.onchange('sale_line_id') + def _onchange_sale_line_id(self): + if self.timesheet_ids: + if self.sale_line_id: + message = _("All timesheet hours that are not yet invoiced will be assigned to the selected Sales Order Item on save. Discard to avoid the change.") + else: + message = _("All timesheet hours that are not yet invoiced will be removed from the selected Sales Order Item on save. Discard to avoid the change.") + + return {'warning': { + 'title': _("Warning"), + 'message': message + }} + @api.depends('sale_line_id', 'project_id', 'billable_type') def _compute_sale_order_id(self): for task in self: @@ -166,11 +179,19 @@ class ProjectTask(models.Model): # set domain on SO: on non billable project, all SOL of customer, otherwise the one from the SO def write(self, values): + old_sale_line_id = dict([(t.id, t.sale_line_id.id) for t in self]) if values.get('project_id'): project_dest = self.env['project.project'].browse(values['project_id']) if project_dest.billable_type == 'employee_rate': values['sale_line_id'] = False - return super(ProjectTask, self).write(values) + res = super(ProjectTask, self).write(values) + if 'sale_line_id' in values and self.sudo().timesheet_ids: + self.timesheet_ids.filtered( + lambda t: (not t.timesheet_invoice_id or t.timesheet_invoice_id.state == 'cancel') and t.so_line.id == old_sale_line_id[t.task_id.id] + ).write({ + 'so_line': values['sale_line_id'] + }) + return res def action_make_billable(self): return { @@ -185,3 +206,6 @@ class ProjectTask(models.Model): 'form_view_initial_mode': 'edit', }, } + + def _get_action_view_so_ids(self): + return list(set((self.sale_order_id + self.timesheet_ids.so_line.order_id).ids)) diff --git a/addons/sale_timesheet/tests/test_sale_service.py b/addons/sale_timesheet/tests/test_sale_service.py index 13413f7c3fa..2f3004920d9 100644 --- a/addons/sale_timesheet/tests/test_sale_service.py +++ b/addons/sale_timesheet/tests/test_sale_service.py @@ -162,15 +162,15 @@ class TestSaleService(TestCommonSaleTimesheetNoChart): self.assertTrue(all([billing_type == 'billable_time' for billing_type in timesheets.mapped('timesheet_invoice_type')]), "All timesheets linked to the task should be on 'billable time'") self.assertEqual(so_line_deliver_global_project.qty_to_invoice, 5, "Quantity to invoice should have been increased when logging timesheet on delivered quantities task") - # make task non billable - task_serv2.write({'sale_line_id': False}) - self.assertTrue(all([billing_type == 'billable_time' for billing_type in timesheets.mapped('timesheet_invoice_type')]), "billable type of timesheet should not change when tranfering task into another project") - self.assertEqual(task_serv2.timesheet_ids.mapped('so_line'), so_line_deliver_global_project, "Old timesheet are not modified when changing the task SO line") - # invoice SO, and validate invoice invoice = self.sale_order._create_invoices()[0] invoice.post() + # make task non billable + task_serv2.write({'sale_line_id': False}) + self.assertTrue(all([billing_type == 'billable_time' for billing_type in timesheets.mapped('timesheet_invoice_type')]), "billable type of timesheet should not change when tranfering task into another project") + self.assertEqual(task_serv2.timesheet_ids.mapped('so_line'), so_line_deliver_global_project, "Old invoiced timesheet are not modified when changing the task SO line") + # try to update timesheets, catch error 'You cannot modify invoiced timesheet' with self.assertRaises(UserError): timesheets.write({'so_line': False}) diff --git a/addons/sale_timesheet/wizard/project_task_create_sale_order.py b/addons/sale_timesheet/wizard/project_task_create_sale_order.py index 9b5e6c38f0a..27d9213268d 100644 --- a/addons/sale_timesheet/wizard/project_task_create_sale_order.py +++ b/addons/sale_timesheet/wizard/project_task_create_sale_order.py @@ -57,9 +57,7 @@ class ProjectTaskCreateSalesOrder(models.TransientModel): if self.task_id.sale_line_id: raise UserError(_("The task is already linked to a sales order item.")) - timesheet_with_so_line = self.env['account.analytic.line'].search_count([('task_id', '=', self.task_id.id), ('so_line', '!=', False), ('project_id', '!=', False)]) - if timesheet_with_so_line: - raise UserError(_('The sales order cannot be created because some timesheets of this task are already linked to another sales order.')) + timesheet_with_so_line = self.env['account.analytic.line'].search([('task_id', '=', self.task_id.id), ('so_line', '!=', False), ('project_id', '!=', False)]) # create SO sale_order = self.env['sale.order'].create({ @@ -76,7 +74,7 @@ class ProjectTaskCreateSalesOrder(models.TransientModel): 'price_unit': self.price_unit, 'project_id': self.task_id.project_id.id, # prevent to re-create a project on confirmation 'task_id': self.task_id.id, - 'product_uom_qty': self.task_id.total_hours_spent, + 'product_uom_qty': self.task_id.total_hours_spent - round(sum(timesheet_with_so_line.mapped('unit_amount')), 2), }) # link task to SOL @@ -87,7 +85,7 @@ class ProjectTaskCreateSalesOrder(models.TransientModel): }) # assign SOL to timesheets - self.env['account.analytic.line'].search([('task_id', '=', self.task_id.id), ('so_line', '=', False)]).write({ + self.env['account.analytic.line'].search([('task_id', '=', self.task_id.id), ('so_line', '=', False), ('timesheet_invoice_id', '=', False)]).write({ 'so_line': sale_order_line.id })