customer_cogs_expense_account/tests/test_income_account_determination.py
2025-11-25 21:43:35 +07:00

396 lines
16 KiB
Python

# -*- coding: utf-8 -*-
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
from hypothesis import given, strategies as st, settings
import time
@tagged('post_install', '-at_install')
class TestIncomeAccountDetermination(TransactionCase):
"""
Property-based tests for income account determination logic.
Tests Properties 4, 5, and 8 from the design document.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
# Get or create test company
cls.company = cls.env.company
# Create unique timestamp for account codes
timestamp = str(int(time.time() * 1000))[-6:]
# Create customer-specific income account
cls.customer_income_account = cls.env['account.account'].create({
'name': 'Customer Specific Income',
'code': f'CUSTINC{timestamp}',
'account_type': 'income',
})
# Create category income account
cls.category_income_account = cls.env['account.account'].create({
'name': 'Category Income',
'code': f'CATINC{timestamp}',
'account_type': 'income',
})
# Create product category with default income account
cls.product_category = cls.env['product.category'].create({
'name': 'Test Category',
'property_account_income_categ_id': cls.category_income_account.id,
})
# Create payment term
cls.payment_term = cls.env.ref('account.account_payment_term_immediate')
@settings(max_examples=10, deadline=None)
@given(
customer_name=st.text(min_size=1, max_size=50, alphabet=st.characters(blacklist_categories=('Cs', 'Cc'))),
product_name=st.text(min_size=1, max_size=50, alphabet=st.characters(blacklist_categories=('Cs', 'Cc'))),
quantity=st.floats(min_value=1.0, max_value=100.0, allow_nan=False, allow_infinity=False),
price=st.floats(min_value=1.0, max_value=1000.0, allow_nan=False, allow_infinity=False),
)
def test_property_4_customer_income_account_precedence(self, customer_name, product_name, quantity, price):
"""
**Feature: customer-cogs-expense-account, Property 4: Customer income account precedence**
For any sales order with a customer that has a defined income account,
all invoice lines should use the customer's income account regardless of product category.
**Validates: Requirements 2.1, 2.6**
"""
# Create customer WITH income account
customer = self.env['res.partner'].create({
'name': customer_name,
'property_account_income_customer_id': self.customer_income_account.id,
})
# Create product with category that has different income account
product = self.env['product.product'].create({
'name': product_name,
'type': 'service', # Use service to avoid stock complications
'categ_id': self.product_category.id,
'list_price': price,
'invoice_policy': 'order',
})
# Create sales order
sale_order = self.env['sale.order'].create({
'partner_id': customer.id,
'partner_invoice_id': customer.id,
'partner_shipping_id': customer.id,
'payment_term_id': self.payment_term.id,
'order_line': [(0, 0, {
'product_id': product.id,
'product_uom_qty': quantity,
'price_unit': price,
})],
})
# Confirm sales order
sale_order.action_confirm()
# Create and post invoice
invoice = sale_order._create_invoices()
invoice.action_post()
# Verify invoice uses customer income account (not category account)
invoice_lines = invoice.line_ids.filtered(lambda l: l.product_id == product)
income_line = invoice_lines.filtered(lambda l: l.account_id.account_type == 'income')
self.assertTrue(
income_line,
f"Invoice should have income account line for product {product_name}"
)
self.assertEqual(
income_line.account_id.id,
self.customer_income_account.id,
f"Invoice should use customer-specific income account (not category account) "
f"for customer '{customer_name}' and product '{product_name}'. "
f"Expected account: {self.customer_income_account.code}, "
f"Got: {income_line.account_id.code}"
)
# Verify audit trail shows customer source
if hasattr(income_line, 'account_source'):
self.assertEqual(
income_line.account_source,
'Customer',
f"Account source should indicate 'Customer' for customer '{customer_name}'"
)
if hasattr(income_line, 'used_customer_specific_account'):
self.assertTrue(
income_line.used_customer_specific_account,
f"Should flag that customer-specific account was used for customer '{customer_name}'"
)
@settings(max_examples=10, deadline=None)
@given(
customer_name=st.text(min_size=1, max_size=50, alphabet=st.characters(blacklist_categories=('Cs', 'Cc'))),
product_name=st.text(min_size=1, max_size=50, alphabet=st.characters(blacklist_categories=('Cs', 'Cc'))),
quantity=st.floats(min_value=1.0, max_value=100.0, allow_nan=False, allow_infinity=False),
price=st.floats(min_value=1.0, max_value=1000.0, allow_nan=False, allow_infinity=False),
)
def test_property_5_income_account_fallback(self, customer_name, product_name, quantity, price):
"""
**Feature: customer-cogs-expense-account, Property 5: Income account fallback**
For any sales order with a customer that has no defined income account,
all invoice lines should use the product category's income account.
**Validates: Requirements 2.2**
"""
# Create customer WITHOUT income account
customer = self.env['res.partner'].create({
'name': customer_name,
# No property_account_income_customer_id set
})
# Create product with category income account
product = self.env['product.product'].create({
'name': product_name,
'type': 'service',
'categ_id': self.product_category.id,
'list_price': price,
'invoice_policy': 'order',
})
# Create sales order
sale_order = self.env['sale.order'].create({
'partner_id': customer.id,
'partner_invoice_id': customer.id,
'partner_shipping_id': customer.id,
'payment_term_id': self.payment_term.id,
'order_line': [(0, 0, {
'product_id': product.id,
'product_uom_qty': quantity,
'price_unit': price,
})],
})
# Confirm sales order
sale_order.action_confirm()
# Create and post invoice
invoice = sale_order._create_invoices()
invoice.action_post()
# Verify invoice uses category income account (fallback)
invoice_lines = invoice.line_ids.filtered(lambda l: l.product_id == product)
income_line = invoice_lines.filtered(lambda l: l.account_id.account_type == 'income')
self.assertTrue(
income_line,
f"Invoice should have income account line for product {product_name}"
)
self.assertEqual(
income_line.account_id.id,
self.category_income_account.id,
f"Invoice should fallback to category income account when customer has no custom account. "
f"Customer: '{customer_name}', Product: '{product_name}'. "
f"Expected account: {self.category_income_account.code}, "
f"Got: {income_line.account_id.code}"
)
# Verify audit trail shows category source
if hasattr(income_line, 'account_source'):
self.assertEqual(
income_line.account_source,
'Product Category',
f"Account source should indicate 'Product Category' for customer '{customer_name}' without custom account"
)
if hasattr(income_line, 'used_customer_specific_account'):
self.assertFalse(
income_line.used_customer_specific_account,
f"Should NOT flag customer-specific account for customer '{customer_name}' without custom account"
)
@settings(max_examples=10, deadline=None)
@given(
customer_name=st.text(min_size=1, max_size=50, alphabet=st.characters(blacklist_categories=('Cs', 'Cc'))),
product_name=st.text(min_size=1, max_size=50, alphabet=st.characters(blacklist_categories=('Cs', 'Cc'))),
has_customer_account=st.booleans(),
quantity=st.floats(min_value=1.0, max_value=100.0, allow_nan=False, allow_infinity=False),
price=st.floats(min_value=1.0, max_value=1000.0, allow_nan=False, allow_infinity=False),
)
def test_property_8_account_determination_consistency(self, customer_name, product_name, has_customer_account, quantity, price):
"""
**Feature: customer-cogs-expense-account, Property 8: Account determination consistency**
For any sales order, the account determination logic should check customer-specific
accounts before product category accounts.
**Validates: Requirements 2.5, 2.8**
"""
# Create customer with or without income account based on random boolean
customer_vals = {'name': customer_name}
if has_customer_account:
customer_vals['property_account_income_customer_id'] = self.customer_income_account.id
customer = self.env['res.partner'].create(customer_vals)
# Create product with category income account
product = self.env['product.product'].create({
'name': product_name,
'type': 'service',
'categ_id': self.product_category.id,
'list_price': price,
'invoice_policy': 'order',
})
# Create sales order
sale_order = self.env['sale.order'].create({
'partner_id': customer.id,
'partner_invoice_id': customer.id,
'partner_shipping_id': customer.id,
'payment_term_id': self.payment_term.id,
'order_line': [(0, 0, {
'product_id': product.id,
'product_uom_qty': quantity,
'price_unit': price,
})],
})
# Confirm sales order
sale_order.action_confirm()
# Create and post invoice
invoice = sale_order._create_invoices()
invoice.action_post()
# Verify invoice uses correct account based on precedence
invoice_lines = invoice.line_ids.filtered(lambda l: l.product_id == product)
income_line = invoice_lines.filtered(lambda l: l.account_id.account_type == 'income')
self.assertTrue(
income_line,
f"Invoice should have income account line for product {product_name}"
)
# Determine expected account based on precedence
if has_customer_account:
expected_account = self.customer_income_account
expected_source = 'Customer'
else:
expected_account = self.category_income_account
expected_source = 'Product Category'
self.assertEqual(
income_line.account_id.id,
expected_account.id,
f"Account determination should follow precedence: customer first, then category. "
f"Customer: '{customer_name}', Has custom account: {has_customer_account}, "
f"Product: '{product_name}'. "
f"Expected account: {expected_account.code}, "
f"Got: {income_line.account_id.code}"
)
# Verify audit trail is consistent with account used
if hasattr(income_line, 'account_source'):
self.assertEqual(
income_line.account_source,
expected_source,
f"Account source should be '{expected_source}' for customer '{customer_name}' "
f"(has_customer_account={has_customer_account})"
)
if hasattr(income_line, 'used_customer_specific_account'):
self.assertEqual(
income_line.used_customer_specific_account,
has_customer_account,
f"Customer-specific account flag should be {has_customer_account} "
f"for customer '{customer_name}'"
)
@settings(max_examples=10, deadline=None)
@given(
customer_name=st.text(min_size=1, max_size=50, alphabet=st.characters(blacklist_categories=('Cs', 'Cc'))),
num_lines=st.integers(min_value=2, max_value=5),
)
def test_property_4_multiple_lines_use_customer_account(self, customer_name, num_lines):
"""
**Feature: customer-cogs-expense-account, Property 4: Customer income account precedence**
For any sales order with multiple lines and a customer that has a defined income account,
ALL invoice lines should use the customer's income account.
**Validates: Requirements 2.6**
"""
# Create customer WITH income account
customer = self.env['res.partner'].create({
'name': customer_name,
'property_account_income_customer_id': self.customer_income_account.id,
})
# Create multiple products
products = []
order_lines = []
for i in range(num_lines):
product = self.env['product.product'].create({
'name': f'Product {i}',
'type': 'service',
'categ_id': self.product_category.id,
'list_price': 100.0 * (i + 1),
'invoice_policy': 'order',
})
products.append(product)
order_lines.append((0, 0, {
'product_id': product.id,
'product_uom_qty': 1.0,
'price_unit': 100.0 * (i + 1),
}))
# Create sales order with multiple lines
sale_order = self.env['sale.order'].create({
'partner_id': customer.id,
'partner_invoice_id': customer.id,
'partner_shipping_id': customer.id,
'payment_term_id': self.payment_term.id,
'order_line': order_lines,
})
# Confirm sales order
sale_order.action_confirm()
# Create and post invoice
invoice = sale_order._create_invoices()
invoice.action_post()
# Verify ALL invoice lines use customer income account
income_lines = invoice.line_ids.filtered(
lambda l: l.account_id.account_type == 'income' and l.product_id
)
self.assertEqual(
len(income_lines),
num_lines,
f"Should have {num_lines} income lines for {num_lines} products"
)
for line in income_lines:
self.assertEqual(
line.account_id.id,
self.customer_income_account.id,
f"ALL invoice lines should use customer-specific income account. "
f"Customer: '{customer_name}', Product: {line.product_id.name}. "
f"Expected account: {self.customer_income_account.code}, "
f"Got: {line.account_id.code}"
)
# Verify audit trail for each line
if hasattr(line, 'account_source'):
self.assertEqual(
line.account_source,
'Customer',
f"All lines should show 'Customer' as account source"
)