From 4fb8c7240d0a8e39b8e65705320362f63df5b25c Mon Sep 17 00:00:00 2001 From: "Xavier Bol (xbo)" Date: Tue, 18 Feb 2025 16:39:21 +0000 Subject: [PATCH] [FIX] sale_timesheet: do not consider the invoice in Invoicing App Legacy Before this commit, the invoice cancelled with payment state set to "Invoicing App Legacy" is actually not really cancelled, it seems to just be a way to create accounting entry. However, the timesheets are considered as not billed if there is no invoice linked to them or if the invoice is cancelled (that is, if the state is set to "Cancelled"), which means, if the invoice is in "legacy status", the timesheets could move in a new invoice since those ones are considered as not billed. This commit adds a condition to determine to know if a timesheet is not billed. The timesheet should now be: - not linked to an invoice - or the invoice linked should be cancelled and payment status should not be "Invoicing App Legacy". Steps to reproduce the issue: ---------------------------- 1. install Accounting, Sales and Timesheets app 2. Create a SO with a service product and confirm the SO 3. Create a timesheet for the SOL contained the service product 4. Create a invoice to partially invoice the service made for the SO. 5. Go to "Accounting app > Settings" 6. Enable the "Invoicing Switch Threshold" feature and make sure the date set on that feature is most recent than the one set on the invoice created in step 4. By doing that, the invoice created in step 4 should now be cancelled but with payment status set to "Invoicing App Legacy". 7. Create another timesheet for the same SOL 8. Go back to the SO created in step 2 and create another invoice to invoice the another timesheet. Current behavior: ---------------- The timesheet linked to the first invoice is now linked to the second one with the other timesheet. Expected behavior: ------------------ Since the first invoice is not really cancelled, the timesheet should still be linked to the invoice and so that timesheet should be considered as billed. opw-4556956 closes odoo/odoo#198786 X-original-commit: 2eaa58fd36b9034e88ffa98eefad4887a652e723 Signed-off-by: Xavier Bol (xbo) --- .../models/account_move_line.py | 4 +- addons/sale_timesheet/models/hr_timesheet.py | 2 +- .../tests/test_project_billing.py | 40 +++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/addons/sale_timesheet/models/account_move_line.py b/addons/sale_timesheet/models/account_move_line.py index d4b185be70d52..37c53066bf2e0 100644 --- a/addons/sale_timesheet/models/account_move_line.py +++ b/addons/sale_timesheet/models/account_move_line.py @@ -19,7 +19,9 @@ def _timesheet_domain_get_invoiced_lines(self, sale_line_delivery): ('project_id', '!=', False), '|', '|', ('timesheet_invoice_id', '=', False), - ('timesheet_invoice_id.state', '=', 'cancel'), + '&', + ('timesheet_invoice_id.state', '=', 'cancel'), + ('timesheet_invoice_id.payment_state', '!=', 'invoicing_legacy'), ('timesheet_invoice_id.payment_state', '=', 'reversed') ] diff --git a/addons/sale_timesheet/models/hr_timesheet.py b/addons/sale_timesheet/models/hr_timesheet.py index b13863fee2e55..449d812e85260 100644 --- a/addons/sale_timesheet/models/hr_timesheet.py +++ b/addons/sale_timesheet/models/hr_timesheet.py @@ -95,7 +95,7 @@ def _is_readonly(self): def _is_not_billed(self): self.ensure_one() - return not self.timesheet_invoice_id or self.timesheet_invoice_id.state == 'cancel' + return not self.timesheet_invoice_id or (self.timesheet_invoice_id.state == 'cancel' and self.timesheet_invoice_id.payment_state != 'invoicing_legacy') def _check_timesheet_can_be_billed(self): return self.so_line in self.project_id.mapped('sale_line_employee_ids.sale_line_id') | self.task_id.sale_line_id | self.project_id.sale_line_id diff --git a/addons/sale_timesheet/tests/test_project_billing.py b/addons/sale_timesheet/tests/test_project_billing.py index fb8450ce8cc24..cebf992a0b847 100644 --- a/addons/sale_timesheet/tests/test_project_billing.py +++ b/addons/sale_timesheet/tests/test_project_billing.py @@ -316,3 +316,43 @@ def test_project_form_view(self): self.assertEqual(project_form.partner_id, self.so.partner_id, 'The partner should be the one defined the SO linked to the SOL defined in the mapping.') project = project_form.save() self.assertEqual(project.pricing_type, 'employee_rate', 'Since there is a mapping in this project, the pricing type should be employee rate.') + + def test_take_into_account_invoicing_app_legacy(self): + """ Test the timesheets linked to a invoice determined as a invoiced imported form app legacy + are still considered as billed even if the state of those invoices is cancelled. + + Since the account_accountant module is not in the dependencies of sale_timesheet module, + this test will manually set the state and payment_status to be in the same condition + than the feature "Invoicing Switch Threshold". + """ + timesheet1 = self.env['account.analytic.line'].create({ + 'name': '/', + 'project_id': self.project_task_rate.id, + 'unit_amount': 1, + 'so_line': self.so1_line_deliver_no_task.id, + 'is_so_line_edited': True, + 'employee_id': self.employee_user.id, + }) + + self.assertEqual(self.so1_line_deliver_no_task.qty_delivered, timesheet1.unit_amount) + invoice1 = self.sale_order_1._create_invoices()[0] + invoice1.action_post() + + self.assertEqual(self.so1_line_deliver_no_task.qty_invoiced, 1) + self.assertEqual(timesheet1.timesheet_invoice_id, invoice1) + + timesheet2 = self.env['account.analytic.line'].create({ + 'project_id': self.project_task_rate.id, + 'unit_amount': 2, + 'so_line': self.so1_line_deliver_no_task.id, + 'is_so_line_edited': True, + 'employee_id': self.employee_user.id, + }) + self.assertEqual(self.so1_line_deliver_no_task.qty_delivered, timesheet1.unit_amount + timesheet2.unit_amount) + invoice1.write({'state': 'cancel', 'payment_state': 'invoicing_legacy'}) + self.assertEqual(self.so1_line_deliver_no_task.qty_invoiced, timesheet1.unit_amount) + invoice2 = self.sale_order_1._create_invoices()[0] + invoice2.action_post() + self.assertEqual(self.so1_line_deliver_no_task.qty_invoiced, timesheet1.unit_amount + timesheet2.unit_amount) + self.assertEqual(timesheet1.timesheet_invoice_id, invoice1) + self.assertEqual(timesheet2.timesheet_invoice_id, invoice2)