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