401 lines
15 KiB
Python
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)"
|
|
)
|