From 64ed12822e075ef6c20ff4a51bc65d788d45457a Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Wed, 25 Feb 2026 16:19:42 +0700 Subject: [PATCH] feat: Expand valid account types for customer income and expense accounts to include receivable, payable, and direct cost types, and update Odoo version in manifest. --- __manifest__.py | 2 +- models/account_move_line.py | 2 -- models/res_partner.py | 20 ++++++++++++-------- tests/test_account_validation.py | 6 ++---- tests/test_form_view.py | 32 ++++++++++++++++++++++++++------ 5 files changed, 41 insertions(+), 21 deletions(-) diff --git a/__manifest__.py b/__manifest__.py index d55547c..c41f76d 100755 --- a/__manifest__.py +++ b/__manifest__.py @@ -5,7 +5,7 @@ 'summary': "Customer-specific income and expense accounts for sales transactions", 'description': """ - This module extends Odoo 18's accounting functionality to support customer-specific + This module extends Odoo 19's accounting functionality to support customer-specific income and expense accounts for sales transactions. By default, Odoo creates journal entries using the income and expense accounts defined in the product category. This module allows defining income and expense accounts at the customer level, which take diff --git a/models/account_move_line.py b/models/account_move_line.py index cd77f03..7d863fb 100755 --- a/models/account_move_line.py +++ b/models/account_move_line.py @@ -102,5 +102,3 @@ class AccountMoveLine(models.Model): "Using standard product category account.", partner.name, partner.id, line.move_id.name or 'draft', str(e) ) - - diff --git a/models/res_partner.py b/models/res_partner.py index c4da43b..f721f31 100755 --- a/models/res_partner.py +++ b/models/res_partner.py @@ -15,7 +15,7 @@ class ResPartner(models.Model): 'account.account', company_dependent=True, string='Customer Income Account', - domain="[('account_type', '=', 'income'), ('active', '=', True)]", + domain="[('account_type', 'in', ('asset_receivable', 'liability_payable', 'income', 'income_other', 'expense', 'expense_direct_cost')), ('active', '=', True)]", help="This account will be used for revenue entries when selling to this customer. " "If not set, the income account from the product category will be used." ) @@ -24,11 +24,13 @@ class ResPartner(models.Model): 'account.account', company_dependent=True, string='Customer Expense Account (COGS)', - domain="[('account_type', '=', 'expense'), ('active', '=', True)]", + domain="[('account_type', 'in', ('asset_receivable', 'liability_payable', 'income', 'income_other', 'expense', 'expense_direct_cost')), ('active', '=', True)]", help="This account will be used for COGS entries when selling to this customer. " "If not set, the expense account from the product category will be used." ) + + def _get_customer_income_account(self): """ Returns the customer-specific income account if defined. @@ -144,16 +146,17 @@ class ResPartner(models.Model): @api.constrains('property_account_income_customer_id') def _check_income_account(self): """ - Validate that the customer income account is of type 'income', + Validate that the customer income account is of a valid type, not deprecated, and belongs to the correct company. """ + valid_types = ('asset_receivable', 'liability_payable', 'income', 'income_other', 'expense', 'expense_direct_cost') for partner in self: account = partner.property_account_income_customer_id if account: # Check account type - if account.account_type != 'income': + if account.account_type not in valid_types: raise ValidationError(_( - "The selected income account '%s' must be of type 'Income'. " + "The selected income account '%s' must be of type 'Income', 'Receivable', 'Payable', or 'Cost of Revenue'. " "Please select a valid income account." ) % account.display_name) @@ -175,16 +178,17 @@ class ResPartner(models.Model): @api.constrains('property_account_expense_customer_id') def _check_expense_account(self): """ - Validate that the customer expense account is of type 'expense', + Validate that the customer expense account is of a valid type, not deprecated, and belongs to the correct company. """ + valid_types = ('asset_receivable', 'liability_payable', 'income', 'income_other', 'expense', 'expense_direct_cost') for partner in self: account = partner.property_account_expense_customer_id if account: # Check account type - if account.account_type != 'expense': + if account.account_type not in valid_types: raise ValidationError(_( - "The selected expense account '%s' must be of type 'Expense'. " + "The selected expense account '%s' must be of type 'Expense', 'Cost of Revenue', 'Receivable', or 'Payable'. " "Please select a valid expense account." ) % account.display_name) diff --git a/tests/test_account_validation.py b/tests/test_account_validation.py index aa12c8d..282253b 100755 --- a/tests/test_account_validation.py +++ b/tests/test_account_validation.py @@ -82,7 +82,7 @@ class TestAccountValidation(TransactionCase): @settings(max_examples=100) @given( customer_name=st.text(min_size=1, max_size=50, alphabet=st.characters(blacklist_categories=('Cs', 'Cc'))), - account_type=st.sampled_from(['asset_current', 'liability_current', 'equity', 'expense']), + account_type=st.sampled_from(['asset_current', 'liability_current', 'equity']), ) def test_property_10_income_account_type_validation(self, customer_name, account_type): """ @@ -98,7 +98,6 @@ class TestAccountValidation(TransactionCase): 'asset_current': self.asset_account, 'liability_current': self.liability_account, 'equity': self.equity_account, - 'expense': self.expense_account, } wrong_account = account_map[account_type] @@ -122,7 +121,7 @@ class TestAccountValidation(TransactionCase): @settings(max_examples=100) @given( customer_name=st.text(min_size=1, max_size=50, alphabet=st.characters(blacklist_categories=('Cs', 'Cc'))), - account_type=st.sampled_from(['asset_current', 'liability_current', 'equity', 'income']), + account_type=st.sampled_from(['asset_current', 'liability_current', 'equity']), ) def test_property_11_expense_account_type_validation(self, customer_name, account_type): """ @@ -138,7 +137,6 @@ class TestAccountValidation(TransactionCase): 'asset_current': self.asset_account, 'liability_current': self.liability_account, 'equity': self.equity_account, - 'income': self.income_account, } wrong_account = account_map[account_type] diff --git a/tests/test_form_view.py b/tests/test_form_view.py index 24f9410..f3c01b8 100755 --- a/tests/test_form_view.py +++ b/tests/test_form_view.py @@ -124,12 +124,22 @@ class TestCustomerFormView(TransactionCase): self.assertIn( 'income', domain_str, - "Income account field domain should filter for income type accounts" + "Income account field domain should include income type accounts" ) self.assertIn( - 'deprecated', + 'asset_receivable', domain_str, - "Income account field domain should filter out deprecated accounts" + "Income account field domain should include receivable type accounts" + ) + self.assertIn( + 'expense_direct_cost', + domain_str, + "Income account field domain should include cost of revenue type accounts" + ) + self.assertIn( + 'active', + domain_str, + "Income account field domain should filter out inactive accounts" ) def test_expense_account_field_domain(self): @@ -160,12 +170,22 @@ class TestCustomerFormView(TransactionCase): self.assertIn( 'expense', domain_str, - "Expense account field domain should filter for expense type accounts" + "Expense account field domain should include expense type accounts" ) self.assertIn( - 'deprecated', + 'liability_payable', domain_str, - "Expense account field domain should filter out deprecated accounts" + "Expense account field domain should include payable type accounts" + ) + self.assertIn( + 'expense_direct_cost', + domain_str, + "Expense account field domain should include cost of revenue type accounts" + ) + self.assertIn( + 'active', + domain_str, + "Expense account field domain should filter out inactive accounts" ) def test_fields_are_optional(self):