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

430 lines
17 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 TestAuditTrail(TransactionCase):
"""
Property-based tests for audit trail functionality.
Tests Properties 13, 14, and 15 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 customer-specific expense account
cls.customer_expense_account = cls.env['account.account'].create({
'name': 'Customer Specific Expense',
'code': f'CUSTEXP{timestamp}',
'account_type': 'expense',
})
# Create category income account
cls.category_income_account = cls.env['account.account'].create({
'name': 'Category Income',
'code': f'CATINC{timestamp}',
'account_type': 'income',
})
# Create category expense account
cls.category_expense_account = cls.env['account.account'].create({
'name': 'Category Expense',
'code': f'CATEXP{timestamp}',
'account_type': 'expense_direct_cost',
})
# Create product category with default accounts
cls.product_category = cls.env['product.category'].create({
'name': 'Test Category',
'property_account_income_categ_id': cls.category_income_account.id,
'property_account_expense_categ_id': cls.category_expense_account.id,
})
# Create stock valuation account
cls.stock_valuation_account = cls.env['account.account'].create({
'name': 'Stock Valuation',
'code': f'STVAL{timestamp}',
'account_type': 'asset_current',
})
# Create stock input account
cls.stock_input_account = cls.env['account.account'].create({
'name': 'Stock Input',
'code': f'STINP{timestamp}',
'account_type': 'asset_current',
})
# Create stock output account
cls.stock_output_account = cls.env['account.account'].create({
'name': 'Stock Output',
'code': f'STOUT{timestamp}',
'account_type': 'asset_current',
})
# Create payment term
cls.payment_term = cls.env.ref('account.account_payment_term_immediate')
# Get warehouse
cls.warehouse = cls.env['stock.warehouse'].search([('company_id', '=', cls.company.id)], limit=1)
if not cls.warehouse:
cls.warehouse = cls.env['stock.warehouse'].create({
'name': 'Test Warehouse',
'code': 'TWH',
'company_id': cls.company.id,
})
@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_13_audit_trail_for_income_accounts(self, customer_name, product_name, quantity, price):
"""
**Feature: customer-cogs-expense-account, Property 13: Audit trail for income accounts**
For any invoice line using a customer-specific income account,
the journal entry metadata should record this information.
**Validates: Requirements 5.1, 5.4**
"""
# 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 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()
# Find the income line
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}"
)
# Verify the line uses customer-specific account
self.assertEqual(
income_line.account_id.id,
self.customer_income_account.id,
f"Income line should use customer-specific account"
)
# Property 13: Verify audit trail metadata is recorded
self.assertTrue(
hasattr(income_line, 'used_customer_specific_account'),
"Journal entry line should have 'used_customer_specific_account' field for audit trail"
)
self.assertTrue(
hasattr(income_line, 'account_source'),
"Journal entry line should have 'account_source' field for audit trail"
)
# Verify the audit metadata shows customer-specific account was used
self.assertTrue(
income_line.used_customer_specific_account,
f"Audit trail should record that customer-specific account was used for customer '{customer_name}'"
)
self.assertEqual(
income_line.account_source,
'Customer',
f"Audit trail should record 'Customer' as account source 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'))),
)
def test_property_14_audit_trail_for_expense_accounts(self, customer_name):
"""
**Feature: customer-cogs-expense-account, Property 14: Audit trail for expense accounts**
For any COGS journal entry using a customer-specific expense account,
the journal entry metadata should record this information.
**Validates: Requirements 5.2, 5.4**
Note: This test verifies that the customer expense account is properly retrieved
and would be used in COGS entries. Full COGS flow testing is covered in
test_expense_account_determination.py
"""
# Create customer WITH expense account
customer = self.env['res.partner'].create({
'name': customer_name,
'property_account_expense_customer_id': self.customer_expense_account.id,
})
# Verify the customer has the expense account set
self.assertEqual(
customer.property_account_expense_customer_id.id,
self.customer_expense_account.id,
f"Customer '{customer_name}' should have expense account set"
)
# Verify the helper method returns the correct account
expense_account = customer._get_customer_expense_account()
self.assertEqual(
expense_account.id,
self.customer_expense_account.id,
f"Customer expense account helper should return the correct account for customer '{customer_name}'"
)
# Property 14: Verify that the expense account information is available for audit
# The actual audit trail for COGS is implemented through the stock move logic
# which uses _get_accounting_data_for_valuation() to inject the customer expense account
# This test verifies the account is properly configured and retrievable
self.assertTrue(
expense_account,
f"Customer '{customer_name}' should have retrievable expense account for audit trail"
)
self.assertEqual(
expense_account.account_type,
'expense',
f"Customer expense account should be of type 'expense' for proper COGS recording"
)
@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_income_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_15_audit_information_visibility(self, customer_name, product_name, has_income_account, quantity, price):
"""
**Feature: customer-cogs-expense-account, Property 15: Audit information visibility**
For any journal entry line, users should be able to view whether
customer-specific accounts were used.
**Validates: Requirements 5.3**
"""
# Create customer with or without income account
customer_vals = {'name': customer_name}
if has_income_account:
customer_vals['property_account_income_customer_id'] = self.customer_income_account.id
customer = self.env['res.partner'].create(customer_vals)
# Create product
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()
# Find the income line
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}"
)
# Property 15: Verify audit information is visible and accessible
self.assertTrue(
hasattr(income_line, 'used_customer_specific_account'),
"Journal entry line should have 'used_customer_specific_account' field visible to users"
)
self.assertTrue(
hasattr(income_line, 'account_source'),
"Journal entry line should have 'account_source' field visible to users"
)
# Verify the audit information is correctly set based on whether customer account was used
if has_income_account:
self.assertTrue(
income_line.used_customer_specific_account,
f"Audit information should show customer-specific account was used for customer '{customer_name}'"
)
self.assertEqual(
income_line.account_source,
'Customer',
f"Audit information should show 'Customer' as source for customer '{customer_name}'"
)
else:
self.assertFalse(
income_line.used_customer_specific_account,
f"Audit information should show customer-specific account was NOT used for customer '{customer_name}'"
)
self.assertEqual(
income_line.account_source,
'Product Category',
f"Audit information should show 'Product Category' as source for customer '{customer_name}'"
)
# Verify the fields are readable (not raising errors)
try:
_ = income_line.used_customer_specific_account
_ = income_line.account_source
except Exception as e:
self.fail(f"Audit trail fields should be readable without errors. Error: {str(e)}")
@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_13_audit_trail_multiple_lines(self, customer_name, num_lines):
"""
**Feature: customer-cogs-expense-account, Property 13: Audit trail for income accounts**
For any invoice with multiple lines using customer-specific income account,
ALL lines should have audit trail metadata recorded.
**Validates: Requirements 5.1, 5.4**
"""
# 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()
# Find all income lines
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"
)
# Verify ALL lines have audit trail metadata
for line in income_lines:
self.assertTrue(
hasattr(line, 'used_customer_specific_account'),
f"Line for product {line.product_id.name} should have audit trail field 'used_customer_specific_account'"
)
self.assertTrue(
hasattr(line, 'account_source'),
f"Line for product {line.product_id.name} should have audit trail field 'account_source'"
)
# Verify audit metadata is correctly set
self.assertTrue(
line.used_customer_specific_account,
f"Line for product {line.product_id.name} should record customer-specific account usage"
)
self.assertEqual(
line.account_source,
'Customer',
f"Line for product {line.product_id.name} should record 'Customer' as account source"
)