[IMP] hr_timesheet: Move not yet invoiced timesheets when project changes

When we change the project on task, we change it also on
timesheet line if this line is not yet invoiced and if it
has no sale order line or the sale order line is the same
as sol of task.

closes odoo/odoo#45154

Taskid: 2169290
Related: odoo/enterprise#9471
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
jbm-odoo
2020-04-16 14:07:30 +00:00
committed by Yannick Tivisse
parent 691bd3c8a8
commit 14b9347343
6 changed files with 135 additions and 13 deletions
+13 -1
View File
@@ -201,11 +201,23 @@ class Task(models.Model):
'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'):
+3 -3
View File
@@ -214,7 +214,7 @@ class TestTimesheet(TestCommonTimesheet):
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 @@ class TestTimesheet(TestCommonTimesheet):
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
+1 -1
View File
@@ -62,7 +62,7 @@ class AccountAnalyticLine(models.Model):
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).'))
+41 -7
View File
@@ -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 @@ class ProjectTask(models.Model):
@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 @@ class ProjectTask(models.Model):
'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 @@ class ProjectTask(models.Model):
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 @@ class ProjectTask(models.Model):
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 @@ class ProjectTask(models.Model):
},
}
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))
@@ -521,3 +521,78 @@ class TestSaleTimesheet(TestCommonSaleTimesheetNoChart):
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")
@@ -105,10 +105,11 @@
</xpath>
<xpath expr="//field[@name='user_id']" position="after">
<field name="is_project_map_empty" invisible="1"/>
<field name="has_multi_sol" invisible="1"/>
</xpath>
<xpath expr="//field[@name='timesheet_ids']/tree" position="inside">
<field name="timesheet_invoice_id" invisible="1"/>
<field name="so_line" readonly="1" attrs="{'column_invisible': ['|', ('parent.is_project_map_empty', '=', True), ('parent.billable_type', '!=', 'employee_rate')]}"/>
<field name="so_line" readonly="1" attrs="{'column_invisible': [('parent.has_multi_sol', '=', False), '|', ('parent.is_project_map_empty', '=', True), ('parent.billable_type', '!=', 'employee_rate')]}"/>
</xpath>
</field>
</record>