diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py
index ebf05c835c2ea..92d019913255f 100644
--- a/addons/hr_timesheet/models/project.py
+++ b/addons/hr_timesheet/models/project.py
@@ -201,11 +201,23 @@ def action_view_subtask_timesheet(self):
'domain': [('project_id', '!=', False), ('task_id', 'in', tasks.ids)],
}
+ def _get_timesheet(self):
+ # Is override in sale_timesheet
+ return self.timesheet_ids
+
def write(self, values):
# a timesheet must have an analytic account (and a project)
if 'project_id' in values and self and not values.get('project_id'):
raise UserError(_('This task must be part of a project because there are some timesheets linked to it.'))
- return super(Task, self).write(values)
+ res = super(Task, self).write(values)
+
+ if 'project_id' in values:
+ project = self.env['project.project'].browse(values.get('project_id'))
+ if project.allow_timesheets:
+ # We write on all non yet invoiced timesheet the new project_id (if project allow timesheet)
+ self._get_timesheet().write({'project_id': values.get('project_id')})
+
+ return res
def name_get(self):
if self.env.context.get('hr_timesheet_display_remaining_hours'):
diff --git a/addons/hr_timesheet/tests/test_timesheet.py b/addons/hr_timesheet/tests/test_timesheet.py
index ab6e6570b88b0..f0cdc38a0da6a 100644
--- a/addons/hr_timesheet/tests/test_timesheet.py
+++ b/addons/hr_timesheet/tests/test_timesheet.py
@@ -214,7 +214,7 @@ def test_create_unlink_project(self):
tracked_project.analytic_account_id.unlink()
def test_transfert_project(self):
- """ Transfert task with timesheet to another project should not modified past timesheets (they are still linked to old project. """
+ """ Transfert task with timesheet to another project. """
Timesheet = self.env['account.analytic.line']
# create a second project
self.project_customer2 = self.env['project.project'].create({
@@ -242,8 +242,8 @@ def test_transfert_project(self):
timesheet_count1 = Timesheet.search_count([('project_id', '=', self.project_customer.id)])
timesheet_count2 = Timesheet.search_count([('project_id', '=', self.project_customer2.id)])
- self.assertEqual(timesheet_count1, 1, "Still one timesheet in project 1")
- self.assertEqual(timesheet_count2, 0, "No timesheet in project 2")
+ self.assertEqual(timesheet_count1, 0, "No timesheet in project 1")
+ self.assertEqual(timesheet_count2, 1, "Still one timesheet in project 2")
self.assertEqual(len(self.task1.timesheet_ids), 1, "The timesheet still should be linked to task 1")
# it is forbidden to set a task with timesheet without project
diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py
index 39d4cd0882e94..b933b65363f90 100644
--- a/addons/sale_timesheet/models/account.py
+++ b/addons/sale_timesheet/models/account.py
@@ -62,7 +62,7 @@ def write(self, values):
return result
def _check_can_write(self, values):
- if self.sudo().filtered(lambda aal: aal.so_line.product_id.invoice_policy == "delivery") and self.filtered(lambda timesheet: timesheet.timesheet_invoice_id):
+ if self.sudo().filtered(lambda aal: aal.so_line.product_id.invoice_policy == "delivery") and self.filtered(lambda t: t.timesheet_invoice_id and t.timesheet_invoice_id.state != 'cancel'):
if any([field_name in values for field_name in ['unit_amount', 'employee_id', 'project_id', 'task_id', 'so_line', 'amount', 'date']]):
raise UserError(_('You can not modify already invoiced timesheets (linked to a Sales order items invoiced on Time and material).'))
diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py
index 64053838b112f..a50ecc7afbdcc 100644
--- a/addons/sale_timesheet/models/project.py
+++ b/addons/sale_timesheet/models/project.py
@@ -174,6 +174,7 @@ class ProjectTask(models.Model):
('no', 'No Billable')
], string="Billable Type", compute='_compute_billable_type', compute_sudo=True, store=True)
is_project_map_empty = fields.Boolean("Is Project map empty", compute='_compute_is_project_map_empty')
+ has_multi_sol = fields.Boolean(compute='_compute_has_multi_sol', compute_sudo=True)
allow_billable = fields.Boolean(related="project_id.allow_billable")
display_create_order = fields.Boolean(compute='_compute_display_create_order')
@@ -191,9 +192,12 @@ def _compute_display_create_order(self):
@api.onchange('sale_line_id')
def _onchange_sale_line_id(self):
- if self.timesheet_ids:
+ if self._get_timesheet() and self.allow_timesheets:
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.")
+ if self.sale_line_id.product_id.service_policy == 'delivered_timesheet' and self._origin.sale_line_id.product_id.service_policy == 'delivered_timesheet':
+ 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 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.")
@@ -202,6 +206,16 @@ def _onchange_sale_line_id(self):
'message': message
}}
+ @api.onchange('project_id')
+ def _onchange_project_id(self):
+ if self._origin.allow_timesheets and self._get_timesheet():
+ message = _("All timesheet hours that are not yet invoiced will be assigned to the selected Project on save. Discard to avoid the change.")
+
+ return {'warning': {
+ 'title': _("Warning"),
+ 'message': message
+ }}
+
@api.depends('analytic_account_id.active')
def _compute_analytic_account_active(self):
super()._compute_analytic_account_active()
@@ -233,6 +247,11 @@ def _compute_is_project_map_empty(self):
for task in self:
task.is_project_map_empty = not bool(task.sudo().project_id.sale_line_employee_ids)
+ @api.depends('timesheet_ids')
+ def _compute_has_multi_sol(self):
+ for task in self:
+ task.has_multi_sol = task.timesheet_ids.so_line != task.sale_line_id
+
@api.onchange('project_id')
def _onchange_project(self):
super(ProjectTask, self)._onchange_project()
@@ -254,12 +273,22 @@ def write(self, values):
if project_dest.billable_type == 'employee_rate':
values['sale_line_id'] = False
res = super(ProjectTask, self).write(values)
- if 'sale_line_id' in values and self.sudo().timesheet_ids:
- self.timesheet_ids.filtered(
+ if 'sale_line_id' in values and self.filtered('allow_timesheets').sudo().timesheet_ids:
+ so = self.env['sale.order.line'].browse(values['sale_line_id']).order_id
+ if so and not so.analytic_account_id:
+ so.analytic_account_id = self.project_id.analytic_account_id
+ timesheet_ids = self.filtered('allow_timesheets').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']
- })
+ )
+ timesheet_ids.write({'so_line': values['sale_line_id']})
+ if 'project_id' in values:
+
+ # Special case when we edit SOL an project in same time, as we edit SOL of
+ # timesheet lines, function '_get_timesheet' won't find the right timesheet
+ # to edit so we must edit those here.
+ project = self.env['project.project'].browse(values.get('project_id'))
+ if project.allow_timesheets:
+ timesheet_ids.write({'project_id': values.get('project_id')})
return res
def action_make_billable(self):
@@ -277,5 +306,10 @@ def action_make_billable(self):
},
}
+ def _get_timesheet(self):
+ # return not invoiced timesheet and timesheet without so_line or so_line linked to task
+ timesheet_ids = super(ProjectTask, self)._get_timesheet()
+ return timesheet_ids.filtered(lambda t: (not t.timesheet_invoice_id or t.timesheet_invoice_id.state == 'cancel') and (not t.so_line or t.so_line == t.task_id._origin.sale_line_id))
+
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_timesheet.py b/addons/sale_timesheet/tests/test_sale_timesheet.py
index 6a57daf629ecf..80986ba24f885 100644
--- a/addons/sale_timesheet/tests/test_sale_timesheet.py
+++ b/addons/sale_timesheet/tests/test_sale_timesheet.py
@@ -521,3 +521,78 @@ def test_timesheet_invoice(self):
self.assertEqual(so_line_deliver_global_project.qty_invoiced, timesheet1.unit_amount + timesheet2.unit_amount + timesheet3.unit_amount)
self.assertTrue(so_line_deliver_task_project.invoice_lines)
self.assertEqual(so_line_deliver_task_project.qty_invoiced, timesheet4.unit_amount)
+
+ def test_transfert_project(self):
+ """ Transfert task with timesheet to another project. """
+ Timesheet = self.env['account.analytic.line']
+ Task = self.env['project.task']
+ today = Date.context_today(self.env.user)
+
+ task = Task.with_context(default_project_id=self.project_global.id).create({
+ 'name': 'first task',
+ 'partner_id': self.partner_customer_usd.id,
+ 'planned_hours': 10,
+ })
+
+ Timesheet.create({
+ 'project_id': self.project_global.id,
+ 'task_id': task.id,
+ 'name': 'my first timesheet',
+ 'unit_amount': 4,
+ })
+
+ timesheet_count1 = Timesheet.search_count([('project_id', '=', self.project_global.id)])
+ timesheet_count2 = Timesheet.search_count([('project_id', '=', self.project_template.id)])
+ self.assertEqual(timesheet_count1, 1, "One timesheet in project_global")
+ self.assertEqual(timesheet_count2, 0, "No timesheet in project_template")
+ self.assertEqual(len(task.timesheet_ids), 1, "The timesheet should be linked to task")
+
+ # change project of task, as the timesheet is not yet invoiced, the timesheet will change his project
+ task.write({
+ 'project_id': self.project_template.id
+ })
+
+ timesheet_count1 = Timesheet.search_count([('project_id', '=', self.project_global.id)])
+ timesheet_count2 = Timesheet.search_count([('project_id', '=', self.project_template.id)])
+ self.assertEqual(timesheet_count1, 0, "No timesheet in project_global")
+ self.assertEqual(timesheet_count2, 1, "One timesheet in project_template")
+ self.assertEqual(len(task.timesheet_ids), 1, "The timesheet still should be linked to task")
+
+ wizard = self.env['project.task.create.sale.order'].with_context(active_id=task.id, active_model='project.task').create({
+ 'product_id': self.product_delivery_timesheet3.id
+ })
+
+ # We create the SO and the invoice
+ action = wizard.action_create_sale_order()
+ sale_order = self.env['sale.order'].browse(action['res_id'])
+ self.context = {
+ 'active_model': 'sale.order',
+ 'active_ids': [sale_order.id],
+ 'active_id': sale_order.id,
+ 'default_journal_id': self.journal_sale.id
+ }
+ wizard = self.env['sale.advance.payment.inv'].with_context(self.context).create({
+ 'advance_payment_method': 'delivered',
+ 'date_invoice_timesheet': today
+ })
+ wizard.create_invoices()
+
+ Timesheet.create({
+ 'project_id': self.project_template.id,
+ 'task_id': task.id,
+ 'name': 'my second timesheet',
+ 'unit_amount': 6,
+ })
+
+ self.assertEqual(Timesheet.search_count([('project_id', '=', self.project_template.id)]), 2, "2 timesheets in project_template")
+
+ # change project of task, the timesheet not yet invoiced will change its project. The timesheet already invoiced will not change his project.
+ task.write({
+ 'project_id': self.project_global.id
+ })
+
+ timesheet_count1 = Timesheet.search_count([('project_id', '=', self.project_global.id)])
+ timesheet_count2 = Timesheet.search_count([('project_id', '=', self.project_template.id)])
+ self.assertEqual(timesheet_count1, 1, "One timesheet in project_global")
+ self.assertEqual(timesheet_count2, 1, "Still one timesheet in project_template")
+ self.assertEqual(len(task.timesheet_ids), 2, "The 2 timesheet still should be linked to task")
diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml
index 51e83ea596953..d933a9e872ed9 100644
--- a/addons/sale_timesheet/views/project_task_views.xml
+++ b/addons/sale_timesheet/views/project_task_views.xml
@@ -105,10 +105,11 @@
+
-
+