Skip to content

Commit

Permalink
[IMP] hr_timesheet: Move not yet invoiced timesheets when project cha…
Browse files Browse the repository at this point in the history
…nges

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#45154

Taskid: 2169290
Related: odoo/enterprise#9471
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
  • Loading branch information
jbm-odoo authored and tivisse committed Apr 16, 2020
1 parent 691bd3c commit 14b9347
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 13 deletions.
14 changes: 13 additions & 1 deletion addons/hr_timesheet/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'):
Expand Down
6 changes: 3 additions & 3 deletions addons/hr_timesheet/tests/test_timesheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion addons/sale_timesheet/models/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).'))

Expand Down
48 changes: 41 additions & 7 deletions addons/sale_timesheet/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand All @@ -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.")

Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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):
Expand All @@ -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))
75 changes: 75 additions & 0 deletions addons/sale_timesheet/tests/test_sale_timesheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
3 changes: 2 additions & 1 deletion addons/sale_timesheet/views/project_task_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down

0 comments on commit 14b9347

Please sign in to comment.