[IMP] sale_timesheet: invoicing timesheets
This commit changes the way timesheet are invoiced. An invoiced timesheet is a timesheet line linked to a invoiced (timesheet_invoice_id set). This happens when the sale order line linked to several timesheets is invoiced, but only for the line based on 'delivered quantity' policy. Those timesheets will be linked to the new created invoice. Those timesheets can not be changed as they are considered as invoiced to the customer: quantity, project, SO line, ... are freezed. Those timesheets are now marked as invoiced at the invoice creation, and not at the invoice validation, like before. This only concerned Customer Invoice (and not Vendor Bills, Credit Note, ....). For timesheets linked to a SO line based on 'ordered quantity', they can be modified as their purpose is only to track the time spend on the SO line, and it does not impact the amount or the quantities invoiced to the customer. The timesheet linked to milestone sale line can be modified too, they are now linked to invoiced, as their is goal purely time tracking (as the delivered quantity is manually change on SO line). This commit also adapt the tests accordingly. Task #1857652
This commit is contained in:
@@ -45,17 +45,23 @@ class AccountInvoice(models.Model):
|
||||
}
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def invoice_validate(self):
|
||||
result = super(AccountInvoice, self).invoice_validate()
|
||||
# associate the invoice to the timesheet on validation
|
||||
for invoice in self:
|
||||
for invoice_line in invoice.invoice_line_ids.filtered(lambda line: line.product_id.type == 'service').sorted(key=lambda inv_line: (inv_line.invoice_id, inv_line.id)):
|
||||
uninvoiced_timesheet_lines = self.env['account.analytic.line'].sudo().search([
|
||||
('so_line', 'in', invoice_line.sale_line_ids.ids),
|
||||
('project_id', '!=', False),
|
||||
('timesheet_invoice_id', '=', False),
|
||||
('timesheet_invoice_type', 'in', ['billable_time', 'billable_fixed'])
|
||||
])
|
||||
uninvoiced_timesheet_lines.write({'timesheet_invoice_id': invoice.id})
|
||||
return result
|
||||
|
||||
class AccountInvoiceLine(models.Model):
|
||||
_inherit = 'account.invoice.line'
|
||||
|
||||
@api.model
|
||||
def create(self, values):
|
||||
""" Link the timesheet from the SO lines to the corresponding draft invoice.
|
||||
NOTE: only the timesheets linked to an Sale Line with a product invoiced on delivered quantity
|
||||
are concerned, since in ordered quantity, the timesheet quantity is not invoiced, but is simply
|
||||
to compute the delivered one (for reporting).
|
||||
"""
|
||||
invoice_line = super(AccountInvoiceLine, self).create(values)
|
||||
if invoice_line.invoice_id.type == 'out_invoice' and invoice_line.invoice_id.state == 'draft':
|
||||
sale_line_delivery = invoice_line.sale_line_ids.filtered(lambda sol: sol.product_id.invoice_policy == 'delivery' and sol.product_id.service_type == 'timesheet')
|
||||
if sale_line_delivery:
|
||||
timesheets = self.env['account.analytic.line'].search([('so_line', 'in', sale_line_delivery.ids), ('timesheet_invoice_id', '=', False), ('project_id', '!=', False)])
|
||||
timesheets.write({
|
||||
'timesheet_invoice_id': invoice_line.invoice_id.id,
|
||||
})
|
||||
return invoice_line
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo.tools import float_is_zero
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
from odoo.addons.sale_timesheet.tests.common import TestCommonSaleTimesheetNoChart
|
||||
|
||||
@@ -63,7 +64,8 @@ class TestSaleTimesheet(TestCommonSaleTimesheetNoChart):
|
||||
self.assertEqual(sale_order.analytic_account_id, project_serv1.analytic_account_id, "The created project should be linked to the analytic account of the SO")
|
||||
|
||||
# create invoice
|
||||
sale_order.action_invoice_create()
|
||||
invoice_id1 = sale_order.action_invoice_create()[0]
|
||||
invoice1 = self.env['account.invoice'].browse(invoice_id1)
|
||||
|
||||
# let's log some timesheets (on the project created by so_line_ordered_project_only)
|
||||
timesheet1 = self.env['account.analytic.line'].create({
|
||||
@@ -76,6 +78,7 @@ class TestSaleTimesheet(TestCommonSaleTimesheetNoChart):
|
||||
self.assertEqual(so_line_ordered_global_project.qty_delivered, 10.5, 'Timesheet directly on project does not increase delivered quantity on so line')
|
||||
self.assertEqual(sale_order.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so')
|
||||
self.assertEqual(timesheet1.timesheet_invoice_type, 'billable_fixed', "Timesheets linked to SO line with ordered product shoulbe be billable fixed")
|
||||
self.assertFalse(timesheet1.timesheet_invoice_id, "The timesheet1 should not be linked to the invoice, since we are in ordered quantity")
|
||||
|
||||
timesheet2 = self.env['account.analytic.line'].create({
|
||||
'name': 'Test Line',
|
||||
@@ -87,6 +90,7 @@ class TestSaleTimesheet(TestCommonSaleTimesheetNoChart):
|
||||
self.assertEqual(so_line_ordered_global_project.qty_delivered, 50, 'Sale Timesheet: timesheet does not increase delivered quantity on so line')
|
||||
self.assertEqual(sale_order.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so')
|
||||
self.assertEqual(timesheet2.timesheet_invoice_type, 'billable_fixed', "Timesheets linked to SO line with ordered product shoulbe be billable fixed")
|
||||
self.assertFalse(timesheet2.timesheet_invoice_id, "The timesheet should not be linked to the invoice, since we are in ordered quantity")
|
||||
|
||||
timesheet3 = self.env['account.analytic.line'].create({
|
||||
'name': 'Test Line',
|
||||
@@ -96,9 +100,10 @@ class TestSaleTimesheet(TestCommonSaleTimesheetNoChart):
|
||||
})
|
||||
self.assertEqual(so_line_ordered_project_only.qty_delivered, 0.0, 'Timesheet directly on project does not increase delivered quantity on so line')
|
||||
self.assertEqual(timesheet3.timesheet_invoice_type, 'non_billable_project', "Timesheets without task shoulbe be 'no project found'")
|
||||
self.assertFalse(timesheet3.timesheet_invoice_id, "The timesheet should not be linked to the invoice, since we are in ordered quantity")
|
||||
|
||||
# log timesheet on task in global project (higher than the initial ordrered qty)
|
||||
self.env['account.analytic.line'].create({
|
||||
timesheet4 = self.env['account.analytic.line'].create({
|
||||
'name': 'Test Line',
|
||||
'project_id': task_serv2.project_id.id,
|
||||
'task_id': task_serv2.id,
|
||||
@@ -106,6 +111,7 @@ class TestSaleTimesheet(TestCommonSaleTimesheetNoChart):
|
||||
'employee_id': self.employee_user.id,
|
||||
})
|
||||
self.assertEqual(sale_order.invoice_status, 'upselling', 'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so')
|
||||
self.assertFalse(timesheet4.timesheet_invoice_id, "The timesheet should not be linked to the invoice, since we are in ordered quantity")
|
||||
|
||||
# add so line with produdct "create task in new project".
|
||||
so_line_ordered_task_new_project = self.env['sale.order.line'].create({
|
||||
@@ -121,11 +127,39 @@ class TestSaleTimesheet(TestCommonSaleTimesheetNoChart):
|
||||
self.assertEqual(sale_order.tasks_count, 2, "Two tasks (1 per SO line) should have been created on SO confirmation")
|
||||
self.assertEqual(len(sale_order.project_ids), 2, "No new project should have been created by the SO, when selling 'new task in new project' product, since it reuse the one from 'project only'.")
|
||||
|
||||
# create invoice
|
||||
invoice_id = sale_order.action_invoice_create()
|
||||
invoice = self.env['account.invoice'].browse(invoice_id)
|
||||
# get first invoice line of sale line linked to timesheet1
|
||||
invoice_line_1 = so_line_ordered_global_project.invoice_lines.filtered(lambda line: line.invoice_id.id == invoice_id1)
|
||||
|
||||
self.assertEqual(so_line_ordered_global_project.product_uom_qty, invoice_line_1.quantity, "The invoice (ordered) quantity should not change when creating timesheet")
|
||||
|
||||
# timesheet can be modified
|
||||
timesheet1.write({'unit_amount': 12})
|
||||
|
||||
self.assertEqual(so_line_ordered_global_project.product_uom_qty, invoice_line_1.quantity, "The invoice (ordered) quantity should not change when modifying timesheet")
|
||||
|
||||
# create second invoice
|
||||
invoice_id2 = sale_order.action_invoice_create()[0]
|
||||
invoice2 = self.env['account.invoice'].browse(invoice_id2)
|
||||
|
||||
self.assertEqual(len(sale_order.invoice_ids), 2, "A second invoice should have been created from the SO")
|
||||
self.assertTrue(float_is_zero(invoice.amount_total - so_line_ordered_task_new_project.price_unit * 3, precision_digits=2), 'Sale: invoice generation on timesheets product is wrong')
|
||||
self.assertTrue(float_is_zero(invoice2.amount_total - so_line_ordered_task_new_project.price_unit * 3, precision_digits=2), 'Sale: invoice generation on timesheets product is wrong')
|
||||
|
||||
self.assertFalse(timesheet1.timesheet_invoice_id, "The timesheet1 should not be linked to the invoice, since we are in ordered quantity")
|
||||
self.assertFalse(timesheet2.timesheet_invoice_id, "The timesheet2 should not be linked to the invoice, since we are in ordered quantity")
|
||||
self.assertFalse(timesheet3.timesheet_invoice_id, "The timesheet3 should not be linked to the invoice, since we are in ordered quantity")
|
||||
self.assertFalse(timesheet4.timesheet_invoice_id, "The timesheet4 should not be linked to the invoice, since we are in ordered quantity")
|
||||
|
||||
# validate the first invoice
|
||||
invoice1.action_invoice_open()
|
||||
|
||||
self.assertEqual(so_line_ordered_global_project.product_uom_qty, invoice_line_1.quantity, "The invoice (ordered) quantity should not change when modifying timesheet")
|
||||
self.assertFalse(timesheet1.timesheet_invoice_id, "The timesheet1 should not be linked to the invoice, since we are in ordered quantity")
|
||||
self.assertFalse(timesheet2.timesheet_invoice_id, "The timesheet2 should not be linked to the invoice, since we are in ordered quantity")
|
||||
self.assertFalse(timesheet3.timesheet_invoice_id, "The timesheet3 should not be linked to the invoice, since we are in ordered quantity")
|
||||
self.assertFalse(timesheet4.timesheet_invoice_id, "The timesheet4 should not be linked to the invoice, since we are in ordered quantity")
|
||||
|
||||
# timesheet can still be modified
|
||||
timesheet1.write({'unit_amount': 13})
|
||||
|
||||
def test_timesheet_delivery(self):
|
||||
""" Test timesheet invoicing with 'invoice on delivery' timetracked products
|
||||
@@ -189,11 +223,15 @@ class TestSaleTimesheet(TestCommonSaleTimesheetNoChart):
|
||||
self.assertEqual(so_line_deliver_task_project.invoice_status, 'no', 'Sale Timesheet: so line invoice status should not change when no timesheet linked to the line')
|
||||
self.assertEqual(sale_order.invoice_status, 'to invoice', 'Sale Timesheet: "invoice on delivery" timesheets should set the so in "to invoice" status when logged')
|
||||
self.assertEqual(timesheet1.timesheet_invoice_type, 'billable_time', "Timesheets linked to SO line with delivered product shoulbe be billable time")
|
||||
self.assertFalse(timesheet1.timesheet_invoice_id, "The timesheet1 should not be linked to the invoice yet")
|
||||
|
||||
# invoice SO
|
||||
invoice_id = sale_order.action_invoice_create()
|
||||
invoice = self.env['account.invoice'].browse(invoice_id)
|
||||
self.assertTrue(float_is_zero(invoice.amount_total - so_line_deliver_global_project.price_unit * 10.5, precision_digits=2), 'Sale: invoice generation on timesheets product is wrong')
|
||||
invoice_id1 = sale_order.action_invoice_create()
|
||||
invoice1 = self.env['account.invoice'].browse(invoice_id1)
|
||||
self.assertTrue(float_is_zero(invoice1.amount_total - so_line_deliver_global_project.price_unit * 10.5, precision_digits=2), 'Sale: invoice generation on timesheets product is wrong')
|
||||
self.assertEqual(timesheet1.timesheet_invoice_id, invoice1, "The timesheet1 should not be linked to the invoice 1, as we are in delivered quantity (even if invoice is in draft")
|
||||
with self.assertRaises(UserError): # We can not modify timesheet linked to invoice (even draft ones)
|
||||
timesheet1.write({'unit_amount': 42})
|
||||
|
||||
# log some timesheets again
|
||||
timesheet2 = self.env['account.analytic.line'].create({
|
||||
@@ -207,11 +245,17 @@ class TestSaleTimesheet(TestCommonSaleTimesheetNoChart):
|
||||
self.assertEqual(so_line_deliver_task_project.invoice_status, 'no', 'Sale Timesheet: so line invoice status should not change when no timesheet linked to the line')
|
||||
self.assertEqual(sale_order.invoice_status, 'to invoice', 'Sale Timesheet: "invoice on delivery" timesheets should not modify the invoice_status of the so')
|
||||
self.assertEqual(timesheet2.timesheet_invoice_type, 'billable_time', "Timesheets linked to SO line with delivered product shoulbe be billable time")
|
||||
self.assertFalse(timesheet2.timesheet_invoice_id, "The timesheet2 should not be linked to the invoice yet")
|
||||
|
||||
# create a second invoice
|
||||
sale_order.action_invoice_create()
|
||||
invoice_id2 = sale_order.action_invoice_create()[0]
|
||||
invoice2 = self.env['account.invoice'].browse(invoice_id2)
|
||||
self.assertEqual(len(sale_order.invoice_ids), 2, "A second invoice should have been created from the SO")
|
||||
self.assertEqual(so_line_deliver_global_project.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on delivery" timesheets should set the so line in "to invoice" status when logged')
|
||||
self.assertEqual(sale_order.invoice_status, 'no', 'Sale Timesheet: "invoice on delivery" timesheets should be invoiced completely by now')
|
||||
self.assertEqual(timesheet2.timesheet_invoice_id, invoice2, "The timesheet2 should not be linked to the invoice 2")
|
||||
with self.assertRaises(UserError): # We can not modify timesheet linked to invoice (even draft ones)
|
||||
timesheet2.write({'unit_amount': 42})
|
||||
|
||||
# add a line on SO
|
||||
so_line_deliver_only_project = self.env['sale.order.line'].create({
|
||||
@@ -227,13 +271,37 @@ class TestSaleTimesheet(TestCommonSaleTimesheetNoChart):
|
||||
# let's log some timesheets on the project
|
||||
timesheet3 = self.env['account.analytic.line'].create({
|
||||
'name': 'Test Line',
|
||||
'project_id': project_serv2.id, # global project
|
||||
'project_id': project_serv2.id,
|
||||
'unit_amount': 7,
|
||||
'employee_id': self.employee_user.id,
|
||||
})
|
||||
self.assertTrue(float_is_zero(so_line_deliver_only_project.qty_delivered, precision_digits=2), "Timesheeting on project should not incremented the delivered quantity on the SO line")
|
||||
self.assertEqual(sale_order.invoice_status, 'no', 'Sale Timesheet: "invoice on delivery" timesheets should be invoiced completely by now')
|
||||
self.assertEqual(timesheet3.timesheet_invoice_type, 'non_billable_project', "Timesheets without task shoulbe be 'no project found'")
|
||||
self.assertFalse(timesheet3.timesheet_invoice_id, "The timesheet3 should not be linked to the invoice yet")
|
||||
|
||||
# let's log some timesheets on the task (new task/new project)
|
||||
timesheet4 = self.env['account.analytic.line'].create({
|
||||
'name': 'Test Line 4',
|
||||
'project_id': task_serv2.project_id.id,
|
||||
'task_id': task_serv2.id,
|
||||
'unit_amount': 7,
|
||||
'employee_id': self.employee_user.id,
|
||||
})
|
||||
self.assertFalse(timesheet4.timesheet_invoice_id, "The timesheet4 should not be linked to the invoice yet")
|
||||
|
||||
# modify a non invoiced timesheet
|
||||
timesheet4.write({'unit_amount': 42})
|
||||
|
||||
self.assertFalse(timesheet4.timesheet_invoice_id, "The timesheet4 should not still be linked to the invoice")
|
||||
|
||||
# validate the second invoice
|
||||
invoice2.action_invoice_open()
|
||||
|
||||
self.assertEqual(timesheet1.timesheet_invoice_id, invoice1, "The timesheet1 should not be linked to the invoice 1, even after validation")
|
||||
self.assertEqual(timesheet2.timesheet_invoice_id, invoice2, "The timesheet2 should not be linked to the invoice 1, even after validation")
|
||||
self.assertFalse(timesheet3.timesheet_invoice_id, "The timesheet3 should not be linked to the invoice, since we are in ordered quantity")
|
||||
self.assertFalse(timesheet4.timesheet_invoice_id, "The timesheet4 should not be linked to the invoice, since we are in ordered quantity")
|
||||
|
||||
def test_timesheet_manual(self):
|
||||
""" Test timesheet invoicing with 'invoice on delivery' timetracked products
|
||||
@@ -297,3 +365,22 @@ class TestSaleTimesheet(TestCommonSaleTimesheetNoChart):
|
||||
|
||||
self.assertEqual(timesheet1.timesheet_invoice_type, 'billable_fixed', "Timesheets linked to SO line with ordered product shoulbe be billable fixed since it is a milestone")
|
||||
self.assertEqual(timesheet2.timesheet_invoice_type, 'non_billable_project', "Timesheets without task shoulbe be 'no project found'")
|
||||
self.assertFalse(timesheet1.timesheet_invoice_id, "The timesheet1 should not be linked to the invoice")
|
||||
self.assertFalse(timesheet2.timesheet_invoice_id, "The timesheet2 should not be linked to the invoice")
|
||||
|
||||
# invoice SO
|
||||
sale_order.order_line.write({'qty_delivered': 5})
|
||||
invoice_id1 = sale_order.action_invoice_create()
|
||||
invoice1 = self.env['account.invoice'].browse(invoice_id1)
|
||||
|
||||
for invoice_line in invoice1.invoice_line_ids:
|
||||
self.assertEqual(invoice_line.quantity, 5, "The invoiced quantity should be 5, as manually set on SO lines")
|
||||
|
||||
self.assertFalse(timesheet1.timesheet_invoice_id, "The timesheet1 should not be linked to the invoice, since timesheets are used for time tracking in milestone")
|
||||
self.assertFalse(timesheet2.timesheet_invoice_id, "The timesheet2 should not be linked to the invoice, since timesheets are used for time tracking in milestone")
|
||||
|
||||
# validate the invoice
|
||||
invoice1.action_invoice_open()
|
||||
|
||||
self.assertFalse(timesheet1.timesheet_invoice_id, "The timesheet1 should not be linked to the invoice, even after invoice validation")
|
||||
self.assertFalse(timesheet2.timesheet_invoice_id, "The timesheet2 should not be linked to the invoice, even after invoice validation")
|
||||
|
||||
Reference in New Issue
Block a user