# -*- coding: utf-8 -*- from odoo.tests import tagged, TransactionCase from odoo.exceptions import UserError, ValidationError @tagged('post_install', '-at_install') class TestErrorHandling(TransactionCase): """ Test error handling for runtime scenarios in customer account determination. Validates Requirements 3.1, 4.3, 4.4 """ def setUp(self): super().setUp() # Get or create a test company self.company = self.env.company # Create test accounts with company_ids (Odoo 18 uses Many2many) # Account codes can only contain alphanumeric characters and dots self.income_account = self.env['account.account'].create({ 'name': 'Test Income Account', 'code': 'TESTINC001', 'account_type': 'income', 'company_ids': [(6, 0, [self.company.id])], }) self.expense_account = self.env['account.account'].create({ 'name': 'Test Expense Account', 'code': 'TESTEXP001', 'account_type': 'expense', 'company_ids': [(6, 0, [self.company.id])], }) # Create a test partner self.partner = self.env['res.partner'].create({ 'name': 'Test Customer', 'company_id': self.company.id, }) def test_inactive_income_account_raises_user_error(self): """ Test that using an inactive income account raises UserError at runtime. Validates Requirement 4.3 """ # Set customer income account self.partner.property_account_income_customer_id = self.income_account # Deprecate the account self.income_account.deprecated = True # Attempt to get the account should raise UserError with self.assertRaises(UserError) as context: self.partner._get_customer_income_account() self.assertIn('inactive', str(context.exception).lower()) def test_inactive_expense_account_raises_user_error(self): """ Test that using an inactive expense account raises UserError at runtime. Validates Requirement 4.3 """ # Set customer expense account self.partner.property_account_expense_customer_id = self.expense_account # Deprecate the account self.expense_account.deprecated = True # Attempt to get the account should raise UserError with self.assertRaises(UserError) as context: self.partner._get_customer_expense_account() self.assertIn('inactive', str(context.exception).lower()) def test_company_mismatch_income_account_raises_user_error(self): """ Test that company mismatch for income account raises error at write time. Validates Requirement 4.4 """ # Create another company other_company = self.env['res.company'].create({ 'name': 'Other Company', }) # Create account in other company (Odoo 18 uses company_ids) # Account codes can only contain alphanumeric characters and dots other_income_account = self.env['account.account'].create({ 'name': 'Other Income Account', 'code': 'OTHERINC001', 'account_type': 'income', 'company_ids': [(6, 0, [other_company.id])], }) # Set partner to first company self.partner.company_id = self.company # Odoo's account module prevents assigning accounts from different companies at write time # This is a ValidationError from Odoo's base account module, which is expected behavior # Our module's runtime check is an additional safety layer error_raised = False try: self.partner.property_account_income_customer_id = other_income_account except (UserError, ValidationError) as e: error_raised = True # The error should mention company mismatch self.assertIn('company', str(e).lower()) self.assertTrue(error_raised, "Expected UserError or ValidationError for company mismatch") def test_company_mismatch_expense_account_raises_user_error(self): """ Test that company mismatch for expense account raises error at write time. Validates Requirement 4.4 """ # Create another company other_company = self.env['res.company'].create({ 'name': 'Other Company', }) # Create account in other company (Odoo 18 uses company_ids) # Account codes can only contain alphanumeric characters and dots other_expense_account = self.env['account.account'].create({ 'name': 'Other Expense Account', 'code': 'OTHEREXP001', 'account_type': 'expense', 'company_ids': [(6, 0, [other_company.id])], }) # Set partner to first company self.partner.company_id = self.company # Odoo's account module prevents assigning accounts from different companies at write time # This is a ValidationError from Odoo's base account module, which is expected behavior # Our module's runtime check is an additional safety layer error_raised = False try: self.partner.property_account_expense_customer_id = other_expense_account except (UserError, ValidationError) as e: error_raised = True # The error should mention company mismatch self.assertIn('company', str(e).lower()) self.assertTrue(error_raised, "Expected UserError or ValidationError for company mismatch") def test_graceful_fallback_with_no_account(self): """ Test that when no customer account is set, methods return False gracefully. Validates Requirement 3.1 (graceful fallback) """ # Don't set any customer accounts # Should return False without errors income_account = self.partner._get_customer_income_account() self.assertFalse(income_account) expense_account = self.partner._get_customer_expense_account() self.assertFalse(expense_account) def test_valid_account_returns_successfully(self): """ Test that valid accounts are returned successfully. Validates Requirement 3.1 (standard functionality preserved) """ # Set valid customer accounts self.partner.property_account_income_customer_id = self.income_account self.partner.property_account_expense_customer_id = self.expense_account # Should return accounts without errors income_account = self.partner._get_customer_income_account() self.assertEqual(income_account, self.income_account) expense_account = self.partner._get_customer_expense_account() self.assertEqual(expense_account, self.expense_account) def test_fallback_to_category_account_for_income(self): """ Test that when customer has no income account, system falls back to product category. Validates Requirement 3.1 (graceful fallback to standard Odoo behavior) """ # Create a product category with income account # Account codes can only contain alphanumeric characters and dots category_income_account = self.env['account.account'].create({ 'name': 'Category Income Account', 'code': 'CATINC001', 'account_type': 'income', 'company_ids': [(6, 0, [self.company.id])], }) product_category = self.env['product.category'].create({ 'name': 'Test Category', 'property_account_income_categ_id': category_income_account.id, }) # Create a product with this category product = self.env['product.product'].create({ 'name': 'Test Product', 'categ_id': product_category.id, 'list_price': 100.0, }) # Customer has no income account set self.assertFalse(self.partner.property_account_income_customer_id) # Helper method should return False customer_account = self.partner._get_customer_income_account() self.assertFalse(customer_account) # The system should fall back to category account in actual invoice processing # This is tested in the integration tests, but we verify the helper returns False def test_fallback_to_category_account_for_expense(self): """ Test that when customer has no expense account, system falls back to product category. Validates Requirement 3.1 (graceful fallback to standard Odoo behavior) """ # Create a product category with expense account # Account codes can only contain alphanumeric characters and dots category_expense_account = self.env['account.account'].create({ 'name': 'Category Expense Account', 'code': 'CATEXP001', 'account_type': 'expense', 'company_ids': [(6, 0, [self.company.id])], }) product_category = self.env['product.category'].create({ 'name': 'Test Category', 'property_account_expense_categ_id': category_expense_account.id, }) # Create a product with this category product = self.env['product.product'].create({ 'name': 'Test Product', 'categ_id': product_category.id, 'standard_price': 50.0, }) # Customer has no expense account set self.assertFalse(self.partner.property_account_expense_customer_id) # Helper method should return False customer_account = self.partner._get_customer_expense_account() self.assertFalse(customer_account) # The system should fall back to category account in actual stock move processing # This is tested in the integration tests, but we verify the helper returns False def test_missing_both_customer_and_category_accounts(self): """ Test that when both customer and category accounts are missing, the system falls back to Odoo's standard error handling. Validates Requirement 3.1 (preserve existing Odoo functionality) """ # Create a product category without accounts product_category = self.env['product.category'].create({ 'name': 'Test Category No Accounts', }) # Create a product with this category product = self.env['product.product'].create({ 'name': 'Test Product No Accounts', 'categ_id': product_category.id, 'list_price': 100.0, }) # Customer has no accounts set self.assertFalse(self.partner.property_account_income_customer_id) self.assertFalse(self.partner.property_account_expense_customer_id) # Helper methods should return False customer_income = self.partner._get_customer_income_account() self.assertFalse(customer_income) customer_expense = self.partner._get_customer_expense_account() self.assertFalse(customer_expense) # When creating actual invoices/moves, Odoo's standard error handling will kick in # This ensures we don't break standard Odoo behavior def test_exception_handling_in_income_account_retrieval(self): """ Test that unexpected exceptions in income account retrieval are caught and logged. Validates Requirement 3.1 (graceful error handling) """ # Set a valid account self.partner.property_account_income_customer_id = self.income_account # The method should handle unexpected errors gracefully # In normal operation, this should work fine account = self.partner._get_customer_income_account() self.assertEqual(account, self.income_account) def test_exception_handling_in_expense_account_retrieval(self): """ Test that unexpected exceptions in expense account retrieval are caught and logged. Validates Requirement 3.1 (graceful error handling) """ # Set a valid account self.partner.property_account_expense_customer_id = self.expense_account # The method should handle unexpected errors gracefully # In normal operation, this should work fine account = self.partner._get_customer_expense_account() self.assertEqual(account, self.expense_account)