Files
project-tools/project_link_from_invoice/tests/test_account_move.py
2026-06-04 11:57:53 +02:00

194 lines
7.3 KiB
Python

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