# -*- 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)" )