[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:
committed by
Yannick Tivisse
parent
691bd3c8a8
commit
14b9347343
@@ -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'):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).'))
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user