# Copyright 2022 Stéphan Sainléger (Elabore) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import Command from odoo.tests import tagged from odoo.addons.sale_project.tests.common import TestSaleProjectCommon @tagged("post_install", "-at_install") class TestProjectLinkFromInvoice(TestSaleProjectCommon): """Tests for the project_link_from_invoice module. Verifies that invoices correctly expose related project(s) via the computed fields project_ids, project_count and projects_name. """ @classmethod def setUpClass(cls): super().setUpClass() cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) # Service product that creates a task in a new project on SO confirmation cls.service_product = cls.env["product.product"].create( { "name": "Service with Project", "type": "service", "invoice_policy": "order", "service_tracking": "task_in_project", } ) # Partner used for orders and invoices cls.partner = cls.env["res.partner"].create({"name": "Test Partner"}) def _confirm_sale_order(self, product, qty=1): """Create and confirm a sale order with one service line.""" so = self.env["sale.order"].create( { "partner_id": self.partner.id, "order_line": [ Command.create( { "product_id": product.id, "product_uom_qty": qty, } ) ], } ) so.action_confirm() return so def _invoice_sale_order(self, sale_order): """Create and post an invoice from a confirmed sale order.""" invoice_wizard = ( self.env["sale.advance.payment.inv"] .with_context( active_model="sale.order", active_ids=sale_order.ids, active_id=sale_order.id, ) .create({"advance_payment_method": "delivered"}) ) invoices = invoice_wizard._create_invoices(sale_order) invoices.action_post() return invoices # ------------------------------------------------------------------ # Tests # ------------------------------------------------------------------ def test_no_project_on_unrelated_invoice(self): """An invoice not linked to any sale order has no related project.""" invoice = self.env["account.move"].create( { "move_type": "out_invoice", "partner_id": self.partner.id, "invoice_line_ids": [ Command.create( { "name": "Manual line", "quantity": 1, "price_unit": 100.0, } ) ], } ) self.assertEqual(invoice.project_count, 0) self.assertFalse(invoice.project_ids) self.assertEqual(invoice.projects_name, "") def test_single_project_linked_to_invoice(self): """An invoice from a SO that created a task has one related project.""" so = self._confirm_sale_order(self.service_product) # The SO must have generated a task in a project task = so.tasks_ids[:1] self.assertTrue(task, "Sale order confirmation should have created a task") project = task.project_id self.assertTrue(project, "Task should belong to a project") invoice = self._invoice_sale_order(so) self.assertEqual(len(invoice), 1) self.assertEqual(invoice.project_count, 1) self.assertIn(project, invoice.project_ids) self.assertIn(project.name, invoice.projects_name) def test_multiple_projects_linked_to_invoice(self): """An invoice from two SOs with different projects shows both projects.""" so1 = self._confirm_sale_order(self.service_product) so2 = self._confirm_sale_order(self.service_product) project1 = so1.tasks_ids[:1].project_id project2 = so2.tasks_ids[:1].project_id self.assertTrue(project1 and project2) # Ensure the two SOs created different projects self.assertNotEqual(project1, project2) # Create a single invoice covering both sale orders invoice_wizard = ( self.env["sale.advance.payment.inv"] .with_context( active_model="sale.order", active_ids=(so1 + so2).ids, ) .create({"advance_payment_method": "delivered"}) ) invoices = invoice_wizard._create_invoices(so1 + so2) invoices.action_post() # Find the invoice that covers both SO lines combined_invoice = invoices[:1] self.assertGreaterEqual(combined_invoice.project_count, 2) self.assertIn(project1, combined_invoice.project_ids) self.assertIn(project2, combined_invoice.project_ids) def test_action_open_projects_single(self): """action_open_projects returns a form view when there is one project.""" so = self._confirm_sale_order(self.service_product) invoice = self._invoice_sale_order(so) self.assertEqual(invoice.project_count, 1) action = invoice.action_open_projects() self.assertEqual(action["res_id"], invoice.project_ids.id) view_types = [v[1] for v in action["views"]] self.assertIn("form", view_types) self.assertNotIn("list", view_types) def test_action_open_projects_multiple(self): """action_open_projects returns a list view when there are multiple projects.""" so1 = self._confirm_sale_order(self.service_product) so2 = self._confirm_sale_order(self.service_product) invoice_wizard = ( self.env["sale.advance.payment.inv"] .with_context( active_model="sale.order", active_ids=(so1 + so2).ids, ) .create({"advance_payment_method": "delivered"}) ) invoices = invoice_wizard._create_invoices(so1 + so2) invoices.action_post() combined_invoice = invoices[:1] if combined_invoice.project_count < 2: self.skipTest("Combined invoice does not span multiple projects") action = combined_invoice.action_open_projects() view_types = [v[1] for v in action["views"]] self.assertIn("list", view_types) self.assertIn("form", view_types) self.assertNotIn("target", action) def test_projects_name_separator(self): """projects_name joins multiple project names with ' ; '.""" so1 = self._confirm_sale_order(self.service_product) so2 = self._confirm_sale_order(self.service_product) invoice_wizard = ( self.env["sale.advance.payment.inv"] .with_context( active_model="sale.order", active_ids=(so1 + so2).ids, ) .create({"advance_payment_method": "delivered"}) ) invoices = invoice_wizard._create_invoices(so1 + so2) invoices.action_post() combined_invoice = invoices[:1] if combined_invoice.project_count >= 2: self.assertIn(" ; ", combined_invoice.projects_name)