430 lines
17 KiB
Python
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"
|
|
)
|