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.
This commit is contained in:
parent
5ddb93e2ec
commit
64ed12822e
@ -5,7 +5,7 @@
|
|||||||
'summary': "Customer-specific income and expense accounts for sales transactions",
|
'summary': "Customer-specific income and expense accounts for sales transactions",
|
||||||
|
|
||||||
'description': """
|
'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
|
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
|
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
|
module allows defining income and expense accounts at the customer level, which take
|
||||||
|
|||||||
@ -102,5 +102,3 @@ class AccountMoveLine(models.Model):
|
|||||||
"Using standard product category account.",
|
"Using standard product category account.",
|
||||||
partner.name, partner.id, line.move_id.name or 'draft', str(e)
|
partner.name, partner.id, line.move_id.name or 'draft', str(e)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ class ResPartner(models.Model):
|
|||||||
'account.account',
|
'account.account',
|
||||||
company_dependent=True,
|
company_dependent=True,
|
||||||
string='Customer Income Account',
|
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. "
|
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."
|
"If not set, the income account from the product category will be used."
|
||||||
)
|
)
|
||||||
@ -24,11 +24,13 @@ class ResPartner(models.Model):
|
|||||||
'account.account',
|
'account.account',
|
||||||
company_dependent=True,
|
company_dependent=True,
|
||||||
string='Customer Expense Account (COGS)',
|
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. "
|
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."
|
"If not set, the expense account from the product category will be used."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _get_customer_income_account(self):
|
def _get_customer_income_account(self):
|
||||||
"""
|
"""
|
||||||
Returns the customer-specific income account if defined.
|
Returns the customer-specific income account if defined.
|
||||||
@ -144,16 +146,17 @@ class ResPartner(models.Model):
|
|||||||
@api.constrains('property_account_income_customer_id')
|
@api.constrains('property_account_income_customer_id')
|
||||||
def _check_income_account(self):
|
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.
|
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:
|
for partner in self:
|
||||||
account = partner.property_account_income_customer_id
|
account = partner.property_account_income_customer_id
|
||||||
if account:
|
if account:
|
||||||
# Check account type
|
# Check account type
|
||||||
if account.account_type != 'income':
|
if account.account_type not in valid_types:
|
||||||
raise ValidationError(_(
|
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."
|
"Please select a valid income account."
|
||||||
) % account.display_name)
|
) % account.display_name)
|
||||||
|
|
||||||
@ -175,16 +178,17 @@ class ResPartner(models.Model):
|
|||||||
@api.constrains('property_account_expense_customer_id')
|
@api.constrains('property_account_expense_customer_id')
|
||||||
def _check_expense_account(self):
|
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.
|
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:
|
for partner in self:
|
||||||
account = partner.property_account_expense_customer_id
|
account = partner.property_account_expense_customer_id
|
||||||
if account:
|
if account:
|
||||||
# Check account type
|
# Check account type
|
||||||
if account.account_type != 'expense':
|
if account.account_type not in valid_types:
|
||||||
raise ValidationError(_(
|
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."
|
"Please select a valid expense account."
|
||||||
) % account.display_name)
|
) % account.display_name)
|
||||||
|
|
||||||
|
|||||||
@ -82,7 +82,7 @@ class TestAccountValidation(TransactionCase):
|
|||||||
@settings(max_examples=100)
|
@settings(max_examples=100)
|
||||||
@given(
|
@given(
|
||||||
customer_name=st.text(min_size=1, max_size=50, alphabet=st.characters(blacklist_categories=('Cs', 'Cc'))),
|
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):
|
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,
|
'asset_current': self.asset_account,
|
||||||
'liability_current': self.liability_account,
|
'liability_current': self.liability_account,
|
||||||
'equity': self.equity_account,
|
'equity': self.equity_account,
|
||||||
'expense': self.expense_account,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wrong_account = account_map[account_type]
|
wrong_account = account_map[account_type]
|
||||||
@ -122,7 +121,7 @@ class TestAccountValidation(TransactionCase):
|
|||||||
@settings(max_examples=100)
|
@settings(max_examples=100)
|
||||||
@given(
|
@given(
|
||||||
customer_name=st.text(min_size=1, max_size=50, alphabet=st.characters(blacklist_categories=('Cs', 'Cc'))),
|
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):
|
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,
|
'asset_current': self.asset_account,
|
||||||
'liability_current': self.liability_account,
|
'liability_current': self.liability_account,
|
||||||
'equity': self.equity_account,
|
'equity': self.equity_account,
|
||||||
'income': self.income_account,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wrong_account = account_map[account_type]
|
wrong_account = account_map[account_type]
|
||||||
|
|||||||
@ -124,12 +124,22 @@ class TestCustomerFormView(TransactionCase):
|
|||||||
self.assertIn(
|
self.assertIn(
|
||||||
'income',
|
'income',
|
||||||
domain_str,
|
domain_str,
|
||||||
"Income account field domain should filter for income type accounts"
|
"Income account field domain should include income type accounts"
|
||||||
)
|
)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
'deprecated',
|
'asset_receivable',
|
||||||
domain_str,
|
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):
|
def test_expense_account_field_domain(self):
|
||||||
@ -160,12 +170,22 @@ class TestCustomerFormView(TransactionCase):
|
|||||||
self.assertIn(
|
self.assertIn(
|
||||||
'expense',
|
'expense',
|
||||||
domain_str,
|
domain_str,
|
||||||
"Expense account field domain should filter for expense type accounts"
|
"Expense account field domain should include expense type accounts"
|
||||||
)
|
)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
'deprecated',
|
'liability_payable',
|
||||||
domain_str,
|
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):
|
def test_fields_are_optional(self):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user