customer_cogs_expense_account/tests/test_sales_flow_integration.py
2025-11-25 21:43:35 +07:00

401 lines
15 KiB
Python

# -*- coding: utf-8 -*-
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
@tagged('post_install', '-at_install')
class TestSalesFlowIntegration(TransactionCase):
"""
Integration test for complete sales flow with customer-specific accounts.
Tests Requirements 2.1, 2.2, 2.3, 2.4, 3.1
"""
@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 chart of accounts if needed
cls._setup_accounts()
# 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,
'property_cost_method': 'fifo',
'property_valuation': 'real_time',
})
# Create test product template first
product_template = cls.env['product.template'].create({
'name': 'Test Product',
'type': 'consu', # Goods/storable product in Odoo 18
'categ_id': cls.product_category.id,
'list_price': 100.0,
'standard_price': 50.0,
'invoice_policy': 'order',
})
# Get the product variant
cls.product = product_template.product_variant_id
# Create stock location for inventory
cls.stock_location = cls.env.ref('stock.stock_location_stock')
cls.customer_location = cls.env.ref('stock.stock_location_customers')
# Add initial stock using inventory adjustment
# For consumable products, we don't need quants, stock moves will handle it
# Create customer with custom accounts
cls.customer_with_accounts = cls.env['res.partner'].create({
'name': 'Customer With Custom Accounts',
'property_account_income_customer_id': cls.customer_income_account.id,
'property_account_expense_customer_id': cls.customer_expense_account.id,
})
# Create customer without custom accounts (fallback scenario)
cls.customer_without_accounts = cls.env['res.partner'].create({
'name': 'Customer Without Custom Accounts',
})
# Create payment term
cls.payment_term = cls.env.ref('account.account_payment_term_immediate')
@classmethod
def _setup_accounts(cls):
"""Setup chart of accounts for testing"""
import time
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 COGS',
'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 COGS',
'code': f'CATEXP{timestamp}',
'account_type': 'expense',
})
# Create stock accounts for real-time valuation
cls.stock_input_account = cls.env['account.account'].create({
'name': 'Stock Input',
'code': f'STKIN{timestamp}',
'account_type': 'asset_current',
})
cls.stock_output_account = cls.env['account.account'].create({
'name': 'Stock Output',
'code': f'STKOUT{timestamp}',
'account_type': 'asset_current',
})
cls.stock_valuation_account = cls.env['account.account'].create({
'name': 'Stock Valuation',
'code': f'STKVAL{timestamp}',
'account_type': 'asset_current',
})
# Set stock accounts on category
cls.product_category_vals = {
'property_stock_account_input_categ_id': cls.stock_input_account.id,
'property_stock_account_output_categ_id': cls.stock_output_account.id,
'property_stock_valuation_account_id': cls.stock_valuation_account.id,
}
def test_complete_sales_flow_with_customer_accounts(self):
"""
Test complete sales flow: SO -> Invoice -> Delivery with customer-specific accounts.
Validates:
- Requirement 2.1: Invoice uses customer income account
- Requirement 2.3: COGS uses customer expense account
- Requirement 3.1: Standard Odoo functionality preserved
"""
# Update product category with stock accounts
self.product_category.write(self.product_category_vals)
# Create sales order
sale_order = self.env['sale.order'].create({
'partner_id': self.customer_with_accounts.id,
'partner_invoice_id': self.customer_with_accounts.id,
'partner_shipping_id': self.customer_with_accounts.id,
'payment_term_id': self.payment_term.id,
'order_line': [(0, 0, {
'product_id': self.product.id,
'product_uom_qty': 5.0,
'price_unit': 100.0,
})],
})
# Confirm sales order
sale_order.action_confirm()
self.assertEqual(sale_order.state, 'sale', "Sales order should be confirmed")
# Create and post invoice
invoice = sale_order._create_invoices()
invoice.action_post()
self.assertEqual(invoice.state, 'posted', "Invoice should be posted")
# Validate invoice uses customer income account (Requirement 2.1)
invoice_lines = invoice.line_ids.filtered(lambda l: l.product_id == self.product)
income_line = invoice_lines.filtered(lambda l: l.account_id.account_type == 'income')
self.assertTrue(income_line, "Invoice should have income account line")
self.assertEqual(
income_line.account_id.id,
self.customer_income_account.id,
"Invoice should use customer-specific income account (Requirement 2.1)"
)
# Verify audit trail
if hasattr(income_line, 'account_source'):
self.assertEqual(
income_line.account_source,
'Customer',
"Account source should indicate customer-specific account"
)
# Deliver products
picking = sale_order.picking_ids
self.assertTrue(picking, "Sales order should create picking")
# Process delivery
for move in picking.move_ids:
for move_line in move.move_line_ids:
move_line.quantity = move.product_uom_qty
picking.button_validate()
self.assertEqual(picking.state, 'done', "Picking should be completed")
# Validate COGS uses customer expense account (Requirement 2.3)
# Get stock move account moves
stock_moves = picking.move_ids
account_moves = self.env['account.move'].search([
('stock_move_id', 'in', stock_moves.ids)
])
if account_moves:
# Find COGS line (expense account)
cogs_lines = account_moves.line_ids.filtered(
lambda l: l.account_id.account_type == 'expense'
)
if cogs_lines:
self.assertEqual(
cogs_lines[0].account_id.id,
self.customer_expense_account.id,
"COGS should use customer-specific expense account (Requirement 2.3)"
)
def test_sales_flow_fallback_to_category_accounts(self):
"""
Test sales flow with customer WITHOUT custom accounts (fallback scenario).
Validates:
- Requirement 2.2: Fallback to category income account
- Requirement 2.4: Fallback to category expense account
- Requirement 3.1: Standard Odoo functionality preserved
"""
# Update product category with stock accounts
self.product_category.write(self.product_category_vals)
# Create sales order for customer without custom accounts
sale_order = self.env['sale.order'].create({
'partner_id': self.customer_without_accounts.id,
'partner_invoice_id': self.customer_without_accounts.id,
'partner_shipping_id': self.customer_without_accounts.id,
'payment_term_id': self.payment_term.id,
'order_line': [(0, 0, {
'product_id': self.product.id,
'product_uom_qty': 3.0,
'price_unit': 100.0,
})],
})
# Confirm sales order
sale_order.action_confirm()
# Create and post invoice
invoice = sale_order._create_invoices()
invoice.action_post()
# Validate invoice uses category income account (Requirement 2.2)
invoice_lines = invoice.line_ids.filtered(lambda l: l.product_id == self.product)
income_line = invoice_lines.filtered(lambda l: l.account_id.account_type == 'income')
self.assertTrue(income_line, "Invoice should have income account line")
self.assertEqual(
income_line.account_id.id,
self.category_income_account.id,
"Invoice should fallback to category income account (Requirement 2.2)"
)
# Verify audit trail shows category source
if hasattr(income_line, 'account_source'):
self.assertIn(
income_line.account_source,
['Product Category', False, ''],
"Account source should indicate category account or be empty"
)
# Deliver products
picking = sale_order.picking_ids
for move in picking.move_ids:
for move_line in move.move_line_ids:
move_line.quantity = move.product_uom_qty
picking.button_validate()
# Validate COGS uses category expense account (Requirement 2.4)
stock_moves = picking.move_ids
account_moves = self.env['account.move'].search([
('stock_move_id', 'in', stock_moves.ids)
])
if account_moves:
cogs_lines = account_moves.line_ids.filtered(
lambda l: l.account_id.account_type == 'expense'
)
if cogs_lines:
self.assertEqual(
cogs_lines[0].account_id.id,
self.category_expense_account.id,
"COGS should fallback to category expense account (Requirement 2.4)"
)
def test_multiple_order_lines_use_customer_accounts(self):
"""
Test that all order lines use customer accounts when defined.
Validates:
- Requirement 2.6: Customer income account applies to all order lines
- Requirement 2.7: Customer expense account applies to all order lines
"""
# Update product category with stock accounts
self.product_category.write(self.product_category_vals)
# Create second product
product_template2 = self.env['product.template'].create({
'name': 'Test Product 2',
'type': 'consu',
'categ_id': self.product_category.id,
'list_price': 200.0,
'standard_price': 100.0,
'invoice_policy': 'order',
})
product2 = product_template2.product_variant_id
# For consumable products, we don't need quants
# Create sales order with multiple lines
sale_order = self.env['sale.order'].create({
'partner_id': self.customer_with_accounts.id,
'partner_invoice_id': self.customer_with_accounts.id,
'partner_shipping_id': self.customer_with_accounts.id,
'payment_term_id': self.payment_term.id,
'order_line': [
(0, 0, {
'product_id': self.product.id,
'product_uom_qty': 2.0,
'price_unit': 100.0,
}),
(0, 0, {
'product_id': product2.id,
'product_uom_qty': 3.0,
'price_unit': 200.0,
}),
],
})
# Confirm and invoice
sale_order.action_confirm()
invoice = sale_order._create_invoices()
invoice.action_post()
# Validate all invoice lines use customer income account (Requirement 2.6)
income_lines = invoice.line_ids.filtered(
lambda l: l.account_id.account_type == 'income'
)
self.assertEqual(len(income_lines), 2, "Should have 2 income lines")
for line in income_lines:
self.assertEqual(
line.account_id.id,
self.customer_income_account.id,
"All invoice lines should use customer income account (Requirement 2.6)"
)
def test_standard_odoo_functionality_preserved(self):
"""
Test that standard Odoo functionality is preserved (Requirement 3.1).
This test verifies that the module doesn't break existing workflows.
"""
# Update product category with stock accounts
self.product_category.write(self.product_category_vals)
# Create a basic sales order
sale_order = self.env['sale.order'].create({
'partner_id': self.customer_without_accounts.id,
'order_line': [(0, 0, {
'product_id': self.product.id,
'product_uom_qty': 1.0,
'price_unit': 100.0,
})],
})
# Standard workflow should work
sale_order.action_confirm()
self.assertEqual(sale_order.state, 'sale')
invoice = sale_order._create_invoices()
self.assertTrue(invoice, "Invoice creation should work")
invoice.action_post()
self.assertEqual(invoice.state, 'posted', "Invoice posting should work")
picking = sale_order.picking_ids
self.assertTrue(picking, "Picking creation should work")
for move in picking.move_ids:
for move_line in move.move_line_ids:
move_line.quantity = move.product_uom_qty
picking.button_validate()
self.assertEqual(picking.state, 'done', "Delivery should work")
# Verify standard accounting entries are created
self.assertTrue(
invoice.line_ids,
"Standard accounting entries should be created (Requirement 3.1)"
)