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