[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:
jem-odoo
2018-08-02 16:12:39 +02:00
parent 9429a0683d
commit 46b578784c
2 changed files with 118 additions and 25 deletions
+20 -14
View File
@@ -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")