396 lines
16 KiB
Python
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"
|
|
)
|